 .../entity_embed_test/entity_embed_test.module     |   8 +
 tests/src/Kernel/EntityEmbedFilterLegacyTest.php   | 100 ++++
 .../src/Kernel/EntityEmbedFilterOverridesTest.php  | 155 ++++++
 tests/src/Kernel/EntityEmbedFilterTest.php         | 406 +++++++++++++++
 .../EntityEmbedFilterTestBase.php}                 | 544 ++++++---------------
 .../Kernel/EntityEmbedFilterTranslationTest.php    | 113 +++++
 6 files changed, 918 insertions(+), 408 deletions(-)

diff --git a/tests/modules/entity_embed_test/entity_embed_test.module b/tests/modules/entity_embed_test/entity_embed_test.module
index cfe5d55..7950862 100644
--- a/tests/modules/entity_embed_test/entity_embed_test.module
+++ b/tests/modules/entity_embed_test/entity_embed_test.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Session\AccountInterface;
 
@@ -85,3 +86,10 @@ function entity_embed_test_entity_access(EntityInterface $entity, $operation, Ac
     return AccessResult::neutral()->addCacheTags(['foo:' . $entity->id()]);
   }
 }
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function entity_embed_test_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  $build['#attributes']['data-entity-embed-test-view-mode'] = $display->getMode();
+}
diff --git a/tests/src/Kernel/EntityEmbedFilterLegacyTest.php b/tests/src/Kernel/EntityEmbedFilterLegacyTest.php
new file mode 100644
index 0000000..164ea46
--- /dev/null
+++ b/tests/src/Kernel/EntityEmbedFilterLegacyTest.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+/**
+ * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter
+ * @group entity_embed
+ * @group legacy
+ */
+class EntityEmbedFilterLegacyTest extends EntityEmbedFilterTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installConfig('system');
+  }
+
+  /**
+   * Tests BC for `data-entity-uuid`'s predecessor, `data-entity-id`.
+   */
+  public function testEntityIdBackwardsCompatibility() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-id' => 1,
+      'data-view-mode' => 'teaser',
+    ]);
+    $this->applyFilter($content);
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [
+      'data-entity-type' => 'node',
+      'data-entity-id' => 1,
+      'data-view-mode' => 'teaser',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-langcode' => 'en',
+      'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+      'data-entity-embed-display-settings' => 'teaser',
+    ]);
+  }
+
+  /**
+   * Verifies `data-entity-id` is ignored when `data-entity-uuid` is present.
+   */
+  public function testEntityIdIgnoredIfEntityUuidPresent() {
+    $nonsensical_id = $this->randomMachineName();
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-entity-id' => $nonsensical_id,
+      'data-view-mode' => 'teaser',
+    ]);
+    $this->applyFilter($content);
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [
+      'data-entity-type' => 'node',
+      'data-entity-id' => $nonsensical_id,
+      'data-view-mode' => 'teaser',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-langcode' => 'en',
+      'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+      'data-entity-embed-display-settings' => 'teaser',
+    ]);
+  }
+
+  /**
+   * Tests BC for`data-entity-embed-display-settings`'s predecessor.
+   */
+  public function testEntityEmbedSettingsBackwardsCompatibility() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-entity-embed-display' => 'entity_reference:entity_reference_label',
+      'data-entity-embed-settings' => '{"link":"0"}',
+    ]);
+    $this->applyFilter($content);
+    $this->assertCount(0, $this->cssSelect('div.embedded-entity a'));
+    $this->assertSame($this->embeddedEntity->label(), (string) $this->cssSelect('div.embedded-entity')[0]);
+  }
+
+  /**
+   * Tests BC for`data-entity-embed-display="default"`.
+   */
+  public function testEntityEmbedDisplayDefaultBackwardsCompatibility() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-entity-embed-display' => 'default',
+      'data-entity-embed-display-settings' => '{"view_mode":"teaser"}',
+    ]);
+    $this->applyFilter($content);
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+      'data-entity-embed-display-settings' => 'teaser',
+      'data-langcode' => 'en',
+    ]);
+  }
+
+}
diff --git a/tests/src/Kernel/EntityEmbedFilterOverridesTest.php b/tests/src/Kernel/EntityEmbedFilterOverridesTest.php
new file mode 100644
index 0000000..22a4de2
--- /dev/null
+++ b/tests/src/Kernel/EntityEmbedFilterOverridesTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\file\Entity\File;
+use Drupal\media\Entity\Media;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+use Drupal\Tests\TestFileCreationTrait;
+
+/**
+ * Tests that entity embeds can have per-embed overrides for e.g. `alt`.
+ *
+ * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter
+ * @group entity_embed
+ */
+class EntityEmbedFilterOverridesTest extends EntityEmbedFilterTestBase {
+
+  use MediaTypeCreationTrait;
+  use TestFileCreationTrait;
+
+  /**
+   * The image file to use in tests.
+   *
+   * @var \Drupal\file\FileInterface
+   */
+  protected $image;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'file',
+    'image',
+    'media',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installSchema('file', ['file_usage']);
+    $this->installEntitySchema('file');
+    $this->installEntitySchema('media');
+    $this->installConfig('image');
+    $this->installConfig('media');
+    $this->installConfig('system');
+
+    $this->image = File::create([
+      'uri' => $this->getTestFiles('image')[0]->uri,
+      'uid' => 2,
+    ]);
+    $this->image->setPermanent();
+    $this->image->save();
+  }
+
+  /**
+   * Tests overriding of `alt` and `title` for default image field formatter.
+   */
+  public function testOverrideAltAndTitleForImage() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'file',
+      'data-entity-uuid' => $this->image->uuid(),
+      'data-entity-embed-display' => 'image:image',
+      'data-entity-embed-display-settings' => '{"image_style":"","image_link":""}',
+      'alt' => 'This is alt text',
+      'title' => 'This is title text',
+    ]);
+
+    $this->applyFilter($content);
+
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], [
+      'alt' => 'This is alt text',
+      'data-entity-embed-display' => 'image:image',
+      'data-entity-type' => 'file',
+      'data-entity-uuid' => $this->image->uuid(),
+      'title' => 'This is title text',
+      'data-langcode' => 'en',
+    ]);
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity img')[0], [
+      'alt' => 'This is alt text',
+      'title' => 'This is title text',
+    ]);
+  }
+
+  /**
+   * Tests overriding of `alt` and `title` for image media items.
+   */
+  public function testOverridesAltAndTitleForImageMedia() {
+    $this->createMediaType('image', ['id' => 'image']);
+    // The `alt` field property is enabled by default, the `title` one is not.
+    // Since we want to test it, enable it.
+    $source_field = FieldConfig::load('media.image.field_media_image');
+    $source_field->setSetting('title_field', TRUE);
+    $source_field->save();
+    $this->container->get('current_user')
+      ->addRole($this->drupalCreateRole(['view media']));
+
+    $media = Media::create([
+      'bundle' => 'image',
+      'name' => 'Screaming hairy armadillo',
+      'field_media_image' => [
+        [
+          'target_id' => $this->image->id(),
+          'alt' => 'default alt',
+          'title' => 'default title',
+        ],
+      ],
+    ]);
+    $media->save();
+
+    $base = [
+      'data-entity-embed-display' => 'view_mode:media.full',
+      'data-entity-embed-display-settings' => '',
+      'data-entity-type' => 'media',
+      'data-entity-uuid' => $media->uuid(),
+    ];
+    $input = $this->createEmbedCode($base);
+    $input .= $this->createEmbedCode([
+      'alt' => 'alt 1',
+      'title' => 'title 1',
+    ] + $base);
+    $input .= $this->createEmbedCode([
+      'alt' => 'alt 2',
+      'title' => 'title 2',
+    ] + $base);
+    $input .= $this->createEmbedCode([
+      'alt' => 'alt 3',
+      'title' => 'title 3',
+    ] + $base);
+
+    $this->applyFilter($input);
+
+    $img_nodes = $this->cssSelect('img');
+    $this->assertCount(4, $img_nodes);
+    $this->assertHasAttributes($img_nodes[0], [
+      'alt' => 'default alt',
+    ]);
+    $this->assertHasAttributes($img_nodes[1], [
+      'alt' => 'alt 1',
+      'title' => 'title 1',
+    ]);
+    $this->assertHasAttributes($img_nodes[2], [
+      'alt' => 'alt 2',
+      'title' => 'title 2',
+    ]);
+    $this->assertHasAttributes($img_nodes[3], [
+      'alt' => 'alt 3',
+      'title' => 'title 3',
+    ]);
+  }
+
+}
diff --git a/tests/src/Kernel/EntityEmbedFilterTest.php b/tests/src/Kernel/EntityEmbedFilterTest.php
new file mode 100644
index 0000000..1efbb49
--- /dev/null
+++ b/tests/src/Kernel/EntityEmbedFilterTest.php
@@ -0,0 +1,406 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+
+/**
+ * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter
+ * @group entity_embed
+ */
+class EntityEmbedFilterTest extends EntityEmbedFilterTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    // @see entity_embed_test_entity_access()
+    // @see entity_embed_test_entity_view_alter()
+    'entity_embed_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installConfig('system');
+  }
+
+  /**
+   * Ensures entities are rendered with correct data attributes.
+   *
+   * @dataProvider providerTestBasics
+   */
+  public function testBasics(array $embed_attributes, $expected_view_mode, array $expected_attributes) {
+    $content = $this->createEmbedCode($embed_attributes);
+
+    $result = $this->applyFilter($content);
+    $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="' . $expected_view_mode .'"]'));
+    $embedded_entity_container = $this->cssSelect('div.embedded-entity')[0];
+    $this->assertHasAttributes($embedded_entity_container, $expected_attributes);
+
+    // Expected bubbleable metadata.
+    $this->assertSame(['config:filter.format.plain_text', 'config:filter.settings', 'foo:1', 'node:1', 'node_view', 'user:2', 'user_view'], $result->getCacheTags());
+    $this->assertSame(['timezone', 'user.permissions'], $result->getCacheContexts());
+    $this->assertSame(Cache::PERMANENT, $result->getCacheMaxAge());
+    $this->assertSame(['library'], array_keys($result->getAttachments()));
+    $this->assertSame(['entity_embed/caption'], $result->getAttachments()['library']);
+  }
+
+  /**
+   * Data provider for testBasics().
+   */
+  public function providerTestBasics() {
+    return [
+      'data-entity-uuid + data-view-mode=teaser' => [
+        'embed_attributes' => [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'teaser',
+        ],
+        'teaser',
+        'expected_attributes' => [
+          'data-entity-type' => 'node',
+          'data-view-mode' => 'teaser',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-langcode' => 'en',
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => 'teaser',
+        ],
+      ],
+      'data-entity-uuid + data-view-mode=full' => [
+        'embed_attributes' => [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'full',
+        ],
+        'default',
+        'expected_attributes' => [
+          'data-entity-type' => 'node',
+          'data-view-mode' => 'full',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-langcode' => 'en',
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => 'full',
+        ],
+      ],
+      'data-entity-uuid + data-view-mode=default' => [
+        'embed_attributes' => [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'default',
+        ],
+        'default',
+        'expected_attributes' => [
+          'data-entity-type' => 'node',
+          'data-view-mode' => 'default',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-langcode' => 'en',
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => 'default',
+        ],
+      ],
+      'data-entity-uuid + data-entity-embed-display' => [
+        'embed_attributes' => [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => '{"view_mode":"full"}',
+        ],
+        'default',
+        'expected_attributes' => [
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => 'full',
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-langcode' => 'en',
+        ],
+      ],
+      'data-entity-uuid + data-entity-embed-display + data-view-mode ⇒ data-entity-embed-display wins' => [
+        'embed_attributes' => [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-entity-embed-display' => 'default',
+          'data-entity-embed-display-settings' => '{"view_mode":"full"}',
+          'data-view-mode' => 'some-invalid-view-mode',
+        ],
+        'default',
+        'expected_attributes' => [
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => 'full',
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'some-invalid-view-mode',
+          'data-langcode' => 'en',
+        ],
+      ],
+      'custom attributes are retained' => [
+        'embed_attributes' => [
+          'data-foo' => 'bar',
+          'foo' => 'bar',
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'teaser',
+        ],
+        'teaser',
+        'expected_attributes' => [
+          'data-foo' => 'bar',
+          'foo' => 'bar',
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'teaser',
+          'data-langcode' => 'en',
+          'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+          'data-entity-embed-display-settings' => 'teaser',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests that Entity Embed respects entity access by embedding an unpublished entity.
+   *
+   * @dataProvider providerAccessUnpublished
+   */
+  public function testAccessUnpublished($allowed_to_view_unpublished, $expected_rendered, CacheableMetadata $expected_cacheability) {
+    // Unpublish the embedded entity so we can test variations in behavior.
+    $this->embeddedEntity->setUnpublished()->save();
+
+    // Are we testing as a user who is allowed to view the embedded entity?
+    if ($allowed_to_view_unpublished) {
+      $this->container->get('current_user')
+        ->addRole($this->drupalCreateRole(['view own unpublished content']));
+    }
+
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-view-mode' => 'teaser',
+    ]);
+    $result = $this->applyFilter($content);
+
+    if (!$expected_rendered) {
+      $this->assertEmpty($this->getRawContent());
+    }
+    else {
+      $this->assertCount(1, $this->cssSelect('div.embedded-entity'));
+      $this->assertRaw('This node is to be used for embedding in other nodes.');
+    }
+
+    // Expected bubbleable metadata.
+    $this->assertSame($result->getCacheTags(), $expected_cacheability->getCacheTags());
+    $this->assertSame($result->getCacheContexts(), $expected_cacheability->getCacheContexts());
+    $this->assertSame($result->getCacheMaxAge(), $expected_cacheability->getCacheMaxAge());
+    $this->assertSame(['library'], array_keys($result->getAttachments()));
+    $this->assertSame(['entity_embed/caption'], $result->getAttachments()['library']);
+  }
+
+  /**
+   * Data provider for testAccessUnpublished().
+   */
+  public function providerAccessUnpublished() {
+    return [
+      'user cannot access embedded entity' => [
+        FALSE,
+        FALSE,
+        (new CacheableMetadata())
+          ->setCacheTags(['foo:1', 'node:1'])
+          ->setCacheContexts(['user.permissions'])
+          ->setCacheMaxAge(Cache::PERMANENT),
+      ],
+      'user can access embedded entity' => [
+        TRUE,
+        TRUE,
+        (new CacheableMetadata())
+          ->setCacheTags(['config:filter.format.plain_text', 'config:filter.settings', 'foo:1', 'node:1', 'node_view', 'user:2', 'user_view'])
+          ->setCacheContexts(['timezone', 'user', 'user.permissions'])
+          ->setCacheMaxAge(Cache::PERMANENT),
+      ],
+    ];
+  }
+
+  /**
+   * Tests for invalid Entity.
+   *
+   * @dataProvider providerTestInvalidEntity
+   */
+  public function testInvalidEntity(array $embed_attributes) {
+    $content = $this->createEmbedCode($embed_attributes);
+
+    $this->applyFilter($content);
+  }
+
+  /**
+   * Data provider for testInvalidEntity().
+   *
+   * @TODO: add more tests.
+   */
+  public function providerTestInvalidEntity() {
+    return [
+      "Ensure that content that can't be loaded is replaced with warning" => [
+        'embed_attributes' => [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => 'InvalidID',
+          'data-view-mode' => 'teaser',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests that only <drupal-entity> tags are processed.
+   */
+  public function testOnlyDrupalEntityTagProcessed() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => $this->embeddedEntity->uuid(),
+      'data-view-mode' => 'teaser',
+    ]);
+    $content = str_replace('drupal-entity', 'entity-embed', $content);
+
+    $filter_result = $this->processText($content, 'en', ['entity_embed']);
+    // If input equals output, the filter didn't change anything.
+    $this->assertSame($content, $filter_result->getProcessedText());
+  }
+
+  /**
+   * @covers \Drupal\filter\Plugin\Filter\FilterAlign
+   * @covers \Drupal\filter\Plugin\Filter\FilterCaption
+   * @dataProvider providerFilterIntegration
+   */
+  public function testFilterIntegration(array $filter_ids, array $additional_attributes, $verification_selector, $expected_verification_success, array $expected_asset_libraries) {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-view-mode' => 'teaser',
+    ] + $additional_attributes);
+    $expected_attributes = [
+      'data-entity-type' => 'node',
+      'data-view-mode' => 'teaser',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-langcode' => 'en',
+      'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+      'data-entity-embed-display-settings' => 'teaser',
+    ];
+
+    $result = $this->processText($content, 'en', $filter_ids);
+    $this->setRawContent($result->getProcessedText());
+    $this->assertCount($expected_verification_success ? 1 : 0, $this->cssSelect($verification_selector));
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], $expected_attributes);
+
+    // Expected bubbleable metadata.
+    $expected_cacheability = (new CacheableMetadata())
+      ->setCacheTags(['config:filter.format.plain_text', 'config:filter.settings', 'foo:1', 'node:1', 'node_view', 'user:2', 'user_view'])
+      ->setCacheContexts(['timezone', 'user.permissions'])
+      ->setCacheMaxAge(Cache::PERMANENT);
+    $this->assertSame($result->getCacheTags(), $expected_cacheability->getCacheTags());
+    $this->assertSame($result->getCacheContexts(), $expected_cacheability->getCacheContexts());
+    $this->assertSame($result->getCacheMaxAge(), $expected_cacheability->getCacheMaxAge());
+    $this->assertSame(['library'], array_keys($result->getAttachments()));
+    $this->assertSame($result->getAttachments()['library'], $expected_asset_libraries);
+  }
+
+  /**
+   * Data provider for testFilterIntegration().
+   */
+  public function providerFilterIntegration() {
+    $default_asset_libraries = ['entity_embed/caption'];
+
+    $caption_additional_attributes = ['data-caption' => 'Yo.'];
+    $caption_verification_selector = 'figure figcaption';
+    $caption_test_cases = [
+      '`data-caption`; only `entity_embed` ⇒ caption absent' => [
+        ['entity_embed'],
+        $caption_additional_attributes,
+        $caption_verification_selector,
+        FALSE,
+        $default_asset_libraries,
+      ],
+      '`data-caption`; `filter_caption` + `entity_embed` ⇒ caption present' => [
+        ['filter_caption', 'entity_embed'],
+        $caption_additional_attributes,
+        $caption_verification_selector,
+        TRUE,
+        ['filter/caption', 'entity_embed/caption'],
+      ],
+    ];
+
+    $align_additional_attributes = ['data-align' => 'center'];
+    $align_verification_selector = 'drupal-entity.align-center';
+    $align_test_cases = [
+      '`data-align`; `entity_embed` ⇒ alignment absent' => [
+        ['entity_embed'],
+        $align_additional_attributes,
+        $align_verification_selector,
+        FALSE,
+        $default_asset_libraries,
+      ],
+      '`data-align`; `filter_align` + `entity_embed` ⇒ alignment present' => [
+        ['filter_align', 'entity_embed'],
+        $align_additional_attributes,
+        $align_verification_selector,
+        FALSE,
+        $default_asset_libraries,
+      ],
+    ];
+
+    $caption_and_align_test_cases = [
+      '`data-caption` + `data-align`; `filter_align` + `filter_caption` + `entity_embed` ⇒ aligned caption present' => [
+        ['filter_align', 'filter_caption', 'entity_embed'],
+        $align_additional_attributes + $caption_additional_attributes,
+        'figure.align-center figcaption',
+        TRUE,
+        ['filter/caption', 'entity_embed/caption'],
+      ],
+    ];
+
+    return $caption_test_cases + $align_test_cases + $caption_and_align_test_cases;
+  }
+
+  /**
+   * Tests that data-entity-embed-display is preferred over data-view-mode.
+   */
+  public function testEntityEmbedDisplayAttributeVersusViewModeAttribute() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-entity-embed-display' => 'default',
+      'data-entity-embed-display-settings' => '{"view_mode":"full"}',
+      'data-view-mode' => 'some-invalid-view-mode',
+    ]);
+    $this->applyFilter($content);
+    $this->assertRaw($this->embeddedEntity->body->value);
+  }
+
+  /**
+   * Tests the placeholder for missing entities.
+   */
+  public function testMissingEntityPlaceholder() {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => $this->embeddedEntity->uuid(),
+      'data-view-mode' => 'default',
+    ]);
+
+    $this->applyFilter($content);
+    $this->assertRaw($this->embeddedEntity->body->value);
+    $this->assertCount(1, $this->cssSelect('div.embedded-entity'));
+
+    $this->embeddedEntity->delete();
+
+    $this->applyFilter($content);
+    $this->assertNoRaw($this->embeddedEntity->body->value);
+    $this->assertCount(0, $this->cssSelect('div.embedded-entity'));
+    $deleted_embed_warning = $this->cssSelect('img')[0];
+    $this->assertNotEmpty($deleted_embed_warning);
+    $this->assertHasAttributes($deleted_embed_warning, [
+      'alt' => 'Deleted content encountered, site owner alerted.',
+      'src' => file_create_url('core/modules/media/images/icons/no-thumbnail.png'),
+      'title' => 'Deleted content.',
+    ]);
+  }
+
+}
diff --git a/tests/src/Functional/EntityEmbedFilterTest.php b/tests/src/Kernel/EntityEmbedFilterTestBase.php
similarity index 5%
rename from tests/src/Functional/EntityEmbedFilterTest.php
rename to tests/src/Kernel/EntityEmbedFilterTestBase.php
index 1c9be44..1b9fff5 100644
--- a/tests/src/Functional/EntityEmbedFilterTest.php
+++ b/tests/src/Kernel/EntityEmbedFilterTestBase.php
@@ -1,42 +1,60 @@
 <?php
 
-namespace Drupal\Tests\entity_embed\Functional;
+namespace Drupal\Tests\entity_embed\Kernel;
 
 use Drupal\Component\Utility\Html;
-use Drupal\language\Entity\ConfigurableLanguage;
-use Drupal\file\Entity\File;
-use Drupal\media\Entity\Media;
-use Drupal\node\Entity\Node;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
 use Drupal\filter\FilterPluginCollection;
+use Drupal\filter\FilterProcessResult;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
 
 /**
- * Tests the entity_embed filter.
- *
- * @group entity_embed
+ * Base class for Entity Embed filter tests.
  */
-class EntityEmbedFilterTest extends EntityEmbedTestBase {
+abstract class EntityEmbedFilterTestBase extends KernelTestBase {
+
+  use NodeCreationTrait {
+    createNode as drupalCreateNode;
+  }
+  use UserCreationTrait {
+    createUser as drupalCreateUser;
+    createRole as drupalCreateRole;
+  }
+  use ContentTypeCreationTrait {
+    createContentType as drupalCreateContentType;
+  }
 
   /**
-   * Modules to enable.
+   * The UUID to use for the embedded entity.
    *
-   * @var array
+   * @var string
+   */
+  const EMBEDDED_ENTITY_UUID = 'e7a3e1fe-b69b-417e-8ee4-c80cb7640e63';
+
+  /**
+   * {@inheritdoc}
    */
   protected static $modules = [
-    'content_translation',
-    'file',
-    'image',
+    'embed',
     'entity_embed',
-    'entity_embed_test',
+    'field',
+    'filter',
     'node',
-    'ckeditor',
+    'system',
+    'text',
+    'user',
   ];
 
   /**
-   * The entity embed filter.
+   * The sample Node entity to embed.
    *
-   * @var \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter
+   * @var \Drupal\node\NodeInterface
    */
-  protected $filter;
+  protected $embeddedEntity;
 
   /**
    * {@inheritdoc}
@@ -44,400 +62,42 @@ class EntityEmbedFilterTest extends EntityEmbedTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $manager = $this->container->get('plugin.manager.filter');
-    $bag = new FilterPluginCollection($manager, []);
-    $this->filter = $bag->get('entity_embed');
-  }
-
-  /**
-   * Tests the entity_embed filter.
-   *
-   * Ensures that entities are getting rendered when correct data attributes
-   * are passed. Also tests situations when embed fails.
-   */
-  public function testFilter() {
-    $assert_session = $this->assertSession();
-
-    // Tests entity embed using entity ID and view mode.
-    $content = '<drupal-entity data-entity-type="node" data-entity-id="' . $this->node->id() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-id and view-mode';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseNotContains('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertSession()->responseContains($this->node->body->value);
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-    $this->assertSession()->responseContains('<div data-entity-type="node" data-entity-id="1" data-view-mode="teaser" data-entity-uuid="' . $this->node->uuid() . '" data-langcode="en" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="teaser" class="embedded-entity">');
-
-    // Tests that embedded entity is not rendered if not accessible.
-    $this->node->setPublished(FALSE)->save();
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test un-accessible entity embed with entity-id and view-mode';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseNotContains('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertSession()->responseNotContains($this->node->body->value);
-    // Verify placeholder does not appear in the output when embed is
-    // successful.
-    $this->assertSession()->responseNotContains(strip_tags($content));
-    // Tests that embedded entity is displayed to the user who has
-    // the view unpublished content permission.
-    $this->createRole(['view own unpublished content'], 'access_unpublished');
-    $this->webUser->addRole('access_unpublished');
-    $this->webUser->save();
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseNotContains('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertSession()->responseContains($this->node->body->value);
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-    $this->assertSession()->responseContains('<div data-entity-type="node" data-entity-id="1" data-view-mode="teaser" data-entity-uuid="' . $this->node->uuid() . '" data-langcode="en" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="teaser" class="embedded-entity">');
-    $this->webUser->removeRole('access_unpublished');
-    $this->webUser->save();
-    $this->node->setPublished(TRUE)->save();
-
-    // Tests entity embed using entity UUID and view mode.
-    $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-uuid and view-mode';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseNotContains('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertSession()->responseContains($this->node->body->value);
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-    $this->assertSession()->responseContains('<div data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser" data-langcode="en" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="teaser" class="embedded-entity">');
-    $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'foo:' . $this->node->id());
-
-    // Ensure that placeholder is not replaced when embed is unsuccessful.
-    $content = '<drupal-entity data-entity-type="node" data-entity-id="InvalidID" data-view-mode="teaser">This placeholder should be rendered since specified entity does not exists.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test that placeholder is retained when specified entity does not exists';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseNotContains('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-
-    // Ensure that UUID is preferred over ID when both attributes are present.
-    $sample_node = $this->drupalCreateNode();
-    $content = '<drupal-entity data-entity-type="node" data-entity-id="' . $sample_node->id() . '" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test that entity-uuid is preferred over entity-id when both attributes are present';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseNotContains('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertSession()->responseContains($this->node->body->value);
-    $this->assertSession()->responseNotContains($sample_node->body->value);
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-    $this->assertSession()->responseContains('<div data-entity-type="node" data-entity-id="' . $sample_node->id() . '" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser" data-langcode="en" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="teaser" class="embedded-entity">');
-
-    // Test deprecated 'default' Entity Embed Display plugin.
-    $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    // Verify that the embedded node exists in page.
-    $this->assertSession()->responseContains($this->node->body->value);
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-    $this->assertSession()->responseContains('<div data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="teaser" data-langcode="en" class="embedded-entity">');
-
-    // Ensure that Entity Embed Display plugin is preferred over view mode when
-    // both attributes are present.
-    $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="default" data-entity-embed-display-settings=\'{"view_mode":"full"}\' data-view-mode="some-invalid-view-mode" data-align="left" data-caption="test caption">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    // Verify embedded node exists in page with the view mode specified
-    // by entity-embed-settings.
-    $this->assertSession()->responseContains($this->node->body->value);
-    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
-    $this->assertSession()->elementExists('css', 'figure.caption-drupal-entity.align-left div.embedded-entity[data-entity-embed-display="entity_reference:entity_reference_entity_view"][data-entity-embed-display-settings="full"][data-entity-type="node"][data-entity-uuid="' . $this->node->uuid() . '"][data-view-mode="some-invalid-view-mode"][data-langcode="en"]');
-    $this->assertSession()->elementTextContains('css', 'figure.caption-drupal-entity.align-left figcaption', 'test caption');
-
-    // Ensure the embedded node doesn't contain data tags on the full page.
-    $this->drupalGet('node/' . $this->node->id());
-    $this->assertSession()->responseNotContains('data-align="left"');
-    $this->assertSession()->responseNotContains('data-caption="test caption"');
-
-    // Test that tag of container element is not replaced when it's not
-    // <drupal-entity>.
-    $content = '<not-drupal-entity data-entity-type="node" data-entity-id="' . $this->node->id() . '" data-view-mode="teaser">this placeholder should not be rendered.</not-drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'test entity embed with entity-id and view-mode';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalget('node/' . $node->id());
-    $this->assertSession()->responseNotContains($this->node->body->value);
-    $this->assertSession()->responseContains('</not-drupal-entity>');
-    $content = '<div data-entity-type="node" data-entity-id="' . $this->node->id() . '" data-view-mode="teaser">this placeholder should not be rendered.</div>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'test entity embed with entity-id and view-mode';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalget('node/' . $node->id());
-    $this->assertSession()->responseNotContains($this->node->body->value);
-    $this->assertSession()->responseContains('<div data-entity-type="node" data-entity-id');
-
-    // Test that attributes are correctly added when image formatter is used.
-    /** @var \Drupal\file\FileInterface $image */
-    $image = $this->getTestFile('image');
-    $image->setPermanent();
-    $image->save();
-    $content = '<drupal-entity data-entity-type="file" data-entity-uuid="' . $image->uuid() . '" data-entity-embed-display="image:image" data-entity-embed-display-settings=\'{"image_style":"","image_link":""}\' data-align="left" data-caption="test caption" alt="This is alt text" title="This is title text">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'test entity image formatter';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalget('node/' . $node->id());
-    $this->assertSession()->elementExists('css', 'figure.caption-drupal-entity.align-left div.embedded-entity[alt="This is alt text"][data-entity-embed-display="image:image"][data-entity-type="file"][data-entity-uuid="' . $image->uuid() . '"][title="This is title text"][data-langcode="en"] img[src][alt="This is alt text"][title="This is title text"]');
-    $this->assertSession()->elementTextContains('css', 'figure.caption-drupal-entity.align-left figcaption', 'test caption');
-
-    // data-entity-embed-settings is replaced with
-    // data-entity-embed-display-settings. Check to see if
-    // data-entity-embed-settings is still working.
-    $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with data-entity-embed-settings';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->elementExists('css', 'figure.caption-drupal-entity.align-left div.embedded-entity[data-entity-embed-display="entity_reference:entity_reference_label"][data-entity-type="node"][data-entity-uuid="' . $this->node->uuid() . '"][data-langcode="en"]');
-    $this->assertSession()->elementTextContains('css', 'figure.caption-drupal-entity.align-left div.embedded-entity', 'Embed Test Node');
-    $this->assertSession()->elementTextContains('css', 'figure.caption-drupal-entity.align-left figcaption', 'test caption');
-
-    // Tests entity embed using custom attribute and custom data- attribute.
-    $content = '<drupal-entity data-foo="bar" foo="bar" data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser">This placeholder should not be rendered.</drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with custom attributes';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertSession()->responseContains('<div data-foo="bar" foo="bar" data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-view-mode="teaser" data-langcode="en" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="teaser" class="embedded-entity">');
-
-    // Tests the placeholder for missing entities.
-    $embedded_node = $this->drupalCreateNode([
-      'type' => 'page',
-      'title' => 'Embedded node',
-      'body' => [['value' => 'Embedded text content', 'format' => 'custom_format']],
+    $this->installSchema('node', 'node_access');
+    $this->installSchema('system', 'sequences');
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    $this->installConfig('filter');
+    $this->installConfig('node');
+
+    // Create a user with required permissions. Ensure that we don't use user 1
+    // because that user is treated in special ways by access control handlers.
+    $admin_user = $this->drupalCreateUser([]);
+    $user = $this->drupalCreateUser([
+      'access content',
     ]);
-    $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $embedded_node->uuid() . '" data-view-mode="default"></drupal-entity>';
-    $settings = [];
-    $settings['type'] = 'page';
-    $settings['title'] = 'Host node';
-    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $assert_session->pageTextContains('Embedded text content');
-    $assert_session->elementNotExists('css', 'img[alt^="Deleted content encountered, site owner alerted"]');
-    $embedded_node->delete();
-    $this->drupalGet('node/' . $node->id());
-    $assert_session->pageTextNotContains('Embedded text content');
-    $placeholder = $assert_session->elementExists('css', 'img[alt^="Deleted content encountered, site owner alerted"]');
-    $this->assertTrue(strpos($placeholder->getAttribute('src'), 'core/modules/media/images/icons/no-thumbnail.png') > 0);
-  }
+    $this->container->set('current_user', $user);
 
-  /**
-   * Tests the filter in different translation contexts.
-   */
-  public function testTranslation() {
-    $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption">This placeholder should not be rendered.</drupal-entity>';
-
-    ConfigurableLanguage::createFromLangcode('pt-br')->save();
-    $host_entity = $this->drupalCreateNode([
-      'type' => 'page',
+    // Create a sample node to be embedded.
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+    $this->embeddedEntity = $this->drupalCreateNode([
+      'title' => 'Embed Test Node',
       'body' => [
-        'value' => $content,
-        'format' => 'custom_format',
-      ],
-    ]);
-    $this->drupalGet($host_entity->toUrl());
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($host_entity->getTitle());
-    $this->assertSession()->pageTextContains($this->node->getTitle());
-
-    // Translate the host entity, but keep the same body; only change the title.
-    $translated_host_entity = $host_entity->addTranslation('pt-br')
-      ->getTranslation('pt-br')
-      ->setTitle('Em portugues')
-      ->set('body', $host_entity->get('body')->getValue());
-    $translated_host_entity->save();
-    $this->drupalGet('/pt-br/node/' . $host_entity->id());
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($translated_host_entity->getTitle());
-    // The embedded node does not have a Portuguese translation, so it should
-    // display in English.
-    $this->assertSession()->pageTextContains($this->node->getTitle());
-
-    // Translate the embedded entity to the same language as the host entity.
-    $this->node = Node::load($this->node->id());
-    $this->node->addTranslation('pt-br')
-      ->getTranslation('pt-br')
-      ->setTitle('Embed em portugues')
-      ->save();
-    $this->getSession()->reload();
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($translated_host_entity->getTitle());
-    // The translated host entity now should show the matching translation of
-    // the embedded entity.
-    $this->assertSession()->pageTextContains($this->node->getTranslation('pt-br')->getTitle());
-
-    // Change the translated host entity to explicitly embed the untranslated
-    // entity.
-    $translated_host_entity->body->value = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption" data-langcode="' . $host_entity->language()->getId() . '">This placeholder should not be rendered.</drupal-entity>';
-    $translated_host_entity->save();
-    $this->getSession()->reload();
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($translated_host_entity->getTitle());
-    $this->assertSession()->pageTextContains($this->node->getTitle());
-
-    // Change the untranslated host entity to explicitly embed the Portuguese
-    // translation of the embedded entity.
-    $host_entity->body->value = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption" data-langcode="pt-br">This placeholder should not be rendered.</drupal-entity>';
-    $host_entity->save();
-    $this->drupalGet('/node/' . $host_entity->id());
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($host_entity->getTitle());
-    $this->assertSession()->pageTextContains($this->node->getTranslation('pt-br')->getTitle());
-
-    // Change the untranslated host entity to explicitly embed a non-existing
-    // translation of the embedded entity; this should fall back to the default
-    // translation.
-    $host_entity->body->value = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption" data-langcode="nl">This placeholder should not be rendered.</drupal-entity>';
-    $host_entity->save();
-    $this->getSession()->reload();
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($host_entity->getTitle());
-    $this->assertSession()->pageTextContains($this->node->getTitle());
-
-    // Change the translated host entity to explicitly embed a non-existing
-    // translation of the embedded entity; this should fall back to the default
-    // translation.
-    $translated_host_entity->body->value = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label" data-entity-embed-settings=\'{"link":"0"}\' data-align="left" data-caption="test caption" data-langcode="nl">This placeholder should not be rendered.</drupal-entity>';
-    $translated_host_entity->save();
-    $this->drupalGet('/pt-br/node/' . $host_entity->id());
-    $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->pageTextContains($translated_host_entity->getTitle());
-    $this->assertSession()->pageTextContains($this->node->getTitle());
-  }
-
-  /**
-   * Tests that embeds are not render cached, and can have per-embed overrides.
-   */
-  public function testOverridesAndRenderCaching() {
-    \Drupal::service('file_system')->copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://batfish.jpg');
-    /** @var \Drupal\file\FileInterface $file */
-    $file = File::create([
-      'uri' => 'public://batfish.jpg',
-      'uid' => $this->webUser->id(),
-    ]);
-    $file->save();
-
-    $this->createNode([
-      'type' => 'article',
-      'title' => 'Red-lipped batfish',
-    ]);
-
-    $media = Media::create([
-      'bundle' => 'image',
-      'name' => 'Screaming hairy armadillo',
-      'field_media_image' => [
-        [
-          'target_id' => $file->id(),
-          'alt' => 'default alt',
-          'title' => 'default title',
-        ],
+        'value' => 'This node is to be used for embedding in other nodes.',
       ],
-    ]);
-    $media->save();
-
-    // Test the same embed with different alt and title text.
-    $input = $this->createEmbedCode([
-      'alt' => 'alt 1',
-      'title' => 'title 1',
-      'data-embed-button' => 'test_media_entity_embed',
-      'data-entity-embed-display' => 'view_mode:media.embed',
-      'data-entity-embed-display-settings' => '',
-      'data-entity-type' => 'media',
-      'data-entity-uuid' => $media->uuid(),
-      'data-langcode' => 'en',
-    ]);
-    $input .= $this->createEmbedCode([
-      'alt' => 'alt 2',
-      'title' => 'title 2',
-      'data-embed-button' => 'test_media_entity_embed',
-      'data-entity-embed-display' => 'view_mode:media.embed',
-      'data-entity-embed-display-settings' => '',
-      'data-entity-type' => 'media',
-      'data-entity-uuid' => $media->uuid(),
-      'data-langcode' => 'en',
-    ]);
-    $input .= $this->createEmbedCode([
-      'alt' => 'alt 3',
-      'title' => 'title 3',
-      'data-embed-button' => 'test_media_entity_embed',
-      'data-entity-embed-display' => 'view_mode:media.embed',
-      'data-entity-embed-display-settings' => '',
-      'data-entity-type' => 'media',
-      'data-entity-uuid' => $media->uuid(),
-      'data-langcode' => 'en',
-    ]);
-
-    /** @var \Drupal\filter\FilterProcessResult $filter_result */
-    $filter_result = $this->filter->process($input, 'en');
-    $output = $filter_result->getProcessedText();
-    $this->assertNotContains('drupal-entity data-entity-type="media" data-entity', $output);
-    $this->assertNotContains('This placeholder should not be rendered.', $output);
-    $dom = Html::load($output);
-    $xpath = new \DOMXPath($dom);
-
-    $img1 = $xpath->query("//img[contains(@alt, 'alt 1')]")[0];
-    $this->assertNotEmpty($img1);
-    $this->assertHasAttributes($img1, [
-      'alt' => 'alt 1',
-      'title' => 'title 1',
-    ]);
-
-    $img2 = $xpath->query("//img[contains(@alt, 'alt 2')]")[0];
-    $this->assertNotEmpty($img2);
-    $this->assertHasAttributes($img2, [
-      'alt' => 'alt 2',
-      'title' => 'title 2',
-    ]);
-
-    $img3 = $xpath->query("//img[contains(@alt, 'alt 3')]")[0];
-    $this->assertNotEmpty($img3);
-    $this->assertHasAttributes($img3, [
-      'alt' => 'alt 3',
-      'title' => 'title 3',
+      'uuid' => static::EMBEDDED_ENTITY_UUID,
     ]);
   }
 
   /**
-   * Creates an embed code with the given attributes.
+   * Gets an embed code with given attributes.
    *
    * @param array $attributes
-   *   The attributes to set.
+   *   The attributes to add.
    *
    * @return string
-   *   A HTML string containing a <drupal-entity> tag with the given attributes.
+   *   A string containing a drupal-entity dom element.
+   *
+   * @see assertEntityEmbedFilterHasRun()
    */
   protected function createEmbedCode(array $attributes) {
     $dom = Html::load('<drupal-entity>This placeholder should not be rendered.</drupal-entity>');
@@ -450,18 +110,86 @@ class EntityEmbedFilterTest extends EntityEmbedTestBase {
   }
 
   /**
-   * Asserts that the DOMNode object has the given attributes.
+   * Applies the `@Filter=entity_embed` filter to text, pipes to raw content.
+   *
+   * @param string $text
+   *   The text string to be filtered.
+   * @param string $langcode
+   *   The language code of the text to be filtered.
    *
-   * @param \DOMNode $node
-   *   The DOMNode object to check.
+   * @return \Drupal\filter\FilterProcessResult
+   *   The filtered text, wrapped in a FilterProcessResult object, and possibly
+   *   with associated assets, cacheability metadata and placeholders.
+   *
+   * @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTestBase::createEmbedCode()
+   * @see \Drupal\KernelTests\AssertContentTrait::setRawContent()
+   */
+  protected function applyFilter($text, $langcode = 'en') {
+    $this->assertContains('<drupal-entity', $text);
+    $this->assertContains('This placeholder should not be rendered.', $text);
+    $filter_result = $this->processText($text, $langcode);
+    $output = $filter_result->getProcessedText();
+    $this->assertNotContains('<drupal-entity', $output);
+    $this->assertNotContains('This placeholder should not be rendered.', $output);
+    $this->setRawContent($output);
+    return $filter_result;
+  }
+
+  /**
+   * Assert that the SimpleXMLElement object has the given attributes.
+   *
+   * @param \SimpleXMLElement $element
+   *   The SimpleXMLElement object to check.
    * @param array $attributes
-   *   An array with attribute names as keys and expected attribute values as
-   *   values.
+   *   An array of attributes.
    */
-  protected function assertHasAttributes(\DOMNode $node, array $attributes) {
+  protected function assertHasAttributes(\SimpleXMLElement $element, array $attributes) {
     foreach ($attributes as $attribute => $value) {
-      $this->assertEquals($value, $node->getAttribute($attribute));
+      $this->assertSame((string) $value, (string) $element[$attribute]);
+    }
+  }
+
+  /**
+   * Processes text through the provided filters.
+   *
+   * @param string $text
+   *   The text string to be filtered.
+   * @param string $langcode
+   *   The language code of the text to be filtered.
+   * @param string[] $filter_ids
+   *   (optional) The filter plugin IDs to apply to the given text, in the order they
+   *   are being requested to be executed.
+   *
+   * @return \Drupal\filter\FilterProcessResult
+   *   The filtered text, wrapped in a FilterProcessResult object, and possibly
+   *   with associated assets, cacheability metadata and placeholders.
+   *
+   * @see \Drupal\filter\Element\ProcessedText::preRenderText()
+   */
+  protected function processText($text, $langcode = 'und', array $filter_ids = ['entity_embed']) {
+    $manager = $this->container->get('plugin.manager.filter');
+    $bag = new FilterPluginCollection($manager, []);
+    $filters = [];
+    foreach ($filter_ids as $filter_id) {
+      $filters[] = $bag->get($filter_id);
+    }
+
+    $render_context = new RenderContext();
+    /** @var \Drupal\filter\FilterProcessResult $filter_result */
+    $filter_result = $this->container->get('renderer')->executeInRenderContext($render_context, function () use ($text, $filters, $langcode) {
+      $metadata = new BubbleableMetadata();
+      foreach ($filters as $filter) {
+        /** @var \Drupal\filter\FilterProcessResult $result */
+        $result = $filter->process($text, $langcode);
+        $metadata = $metadata->merge($result);
+        $text = $result->getProcessedText();
+      }
+      return (new FilterProcessResult($text))->merge($metadata);
+    });
+    if (!$render_context->isEmpty()) {
+      $filter_result = $filter_result->merge($render_context->pop());
     }
+    return $filter_result;
   }
 
 }
diff --git a/tests/src/Kernel/EntityEmbedFilterTranslationTest.php b/tests/src/Kernel/EntityEmbedFilterTranslationTest.php
new file mode 100644
index 0000000..4e96d67
--- /dev/null
+++ b/tests/src/Kernel/EntityEmbedFilterTranslationTest.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * Tests that entity embeds are translated based on host and `data-langcode`.
+ *
+ * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter
+ * @group entity_embed
+ */
+class EntityEmbedFilterTranslationTest extends EntityEmbedFilterTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'language',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    ConfigurableLanguage::createFromLangcode('pt-br')->save();
+    // Reload the entity to ensure it is aware of the newly created language.
+    $this->embeddedEntity = $this->container->get('entity.manager')
+      ->getStorage($this->embeddedEntity->getEntityTypeId())
+      ->load($this->embeddedEntity->id());
+
+    $this->embeddedEntity->addTranslation('pt-br')
+      ->setTitle('Embed em portugues')
+      ->save();
+  }
+
+  /**
+   * Tests that the expected embedded entity translation is selected.
+   *
+   * @dataProvider providerTranslationSituations
+   */
+  public function testTranslationSelection($text_langcode, array $additional_attributes, $expected_title_langcode) {
+    $text = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-view-mode' => 'teaser',
+      'data-entity-embed-display' => 'entity_reference:entity_reference_label',
+      'data-entity-embed-settings' => '{"link":"0"}',
+    ] + $additional_attributes);
+
+    $result = $this->processText($text, $text_langcode, ['entity_embed']);
+    $this->setRawContent($result->getProcessedText());
+
+    $this->assertRaw($this->embeddedEntity->getTranslation($expected_title_langcode)->getTitle());
+    // Verify that the filtered text does not vary by translation-related cache
+    // contexts: a particular translation of the embedded entity is selected
+    // based on either the `data-langcode` attribute or the host entity's
+    // language, neither of which should require a cache context to be
+    // associated. (The host entity's language may itself be selected based on
+    // some request context, but that is of no concern to this filter.)
+    $this->assertSame($result->getCacheContexts(), ['user.permissions']);
+  }
+
+  /**
+   * Data provider for testTranslationSelection().
+   */
+  public function providerTranslationSituations() {
+    $embedded_entity_translation_languages = ['en', 'pt-br'];
+
+    foreach (['en', 'pt-br', 'nl'] as $text_langcode) {
+      // When no `data-langcode` attribute is specified, the text language
+      // (which is set to the host entity's language) is respected. If that
+      // translation does not exist, it falls back to the default translation of
+      // the embedded entity.
+      yield ["text_langcode=$text_langcode (✅) ⇒ $text_langcode" =>
+        $text_langcode,
+        [],
+        in_array($text_langcode, $embedded_entity_translation_languages) ? $text_langcode : 'en',
+      ];
+
+      // When the embedded entity has a translation for the language code in the
+      // `data-langcode` attribute, that translation is used, regardless of the
+      // language of the text (which is set to the language of the host entity).
+      foreach ($embedded_entity_translation_languages as $data_langcode) {
+        yield "text_langcode=$text_langcode (✅); data-langcode=$data_langcode (✅) ⇒ $data_langcode" => [
+          $text_langcode,
+          ['data-langcode' => $data_langcode],
+          $data_langcode,
+        ];
+      }
+
+      // When specifying a (valid) language code but the embedded entity has no
+      // translation for that language, it falls back to the default translation
+      // of the embedded entity.
+      yield "text_langcode=$text_langcode (✅); data-langcode=nl (🚫) ⇒ en" => [
+        $text_langcode,
+        ['data-langcode' => 'nl'],
+        'en',
+      ];
+
+      // When specifying a invalid language code, it falls back to the default
+      // translation of the embedded entity.
+      yield "text_langcode=$text_langcode (✅); data-langcode=non-existing-and-even-invalid-langcode (🚫) ⇒ en" => [
+        $text_langcode,
+        ['data-langcode' => 'non-existing-and-even-invalid-langcode'],
+        'en',
+      ];
+    }
+  }
+
+}
