From 54e5bff56d9368533a84069412e7c003f193ccbc Mon Sep 17 00:00:00 2001
From: Brian Canini <canini.16@osu.edu>
Date: Tue, 30 Jun 2020 10:12:21 -0400
Subject: [PATCH] Updating drupal/embed (1.0.0 => 1.4.0) Updating
 drupal/entity_embed (1.0-beta2 => 1.1.0) Removing entity_embed patches

---
 composer.json                                 |   8 +-
 composer.lock                                 |  68 +-
 vendor/composer/installed.json                |  70 +-
 web/modules/embed/README.md                   |   2 -
 web/modules/embed/composer.json               |  11 -
 .../embed/config/schema/embed.schema.yml      |  12 +-
 web/modules/embed/embed.info.yml              |   9 +-
 web/modules/embed/embed.install               |  14 +
 web/modules/embed/embed.module                |  81 ++
 web/modules/embed/embed.post_update.php       |  41 +
 .../Access/EmbedButtonEditorAccessCheck.php   |   2 +-
 .../embed/src/Controller/EmbedController.php  |  59 +-
 .../embed/src/EmbedButtonInterface.php        |  29 +
 .../embed/src/EmbedCKEditorPluginBase.php     |  23 +-
 .../src/EmbedType/EmbedTypeInterface.php      |   5 +-
 .../embed/src/EmbedType/EmbedTypeManager.php  |   3 +-
 web/modules/embed/src/Entity/EmbedButton.php  | 111 +--
 .../embed/src/Form/EmbedButtonForm.php        |  68 +-
 .../embed/src/Tests/EmbedButtonAdminTest.php  |  84 --
 web/modules/embed/src/Tests/EmbedTestBase.php | 224 -----
 .../embed.button.embed_test_default.yml       |   1 +
 .../tests/embed_test/embed_test.info.yml      |  10 +-
 .../src/Plugin/EmbedType/Aircraft.php         |   2 +-
 .../src/Plugin/EmbedType/Animal.php           |   2 +-
 .../src/Plugin/EmbedType/EmbedTestDefault.php |   2 +-
 .../src/Plugin/Filter/EntityEmbedByID.php     |  67 +-
 .../EmbedButtonEditorAccessCheckTest.php      |  10 +-
 .../tests/src/Functional/EmbedPreviewTest.php |   6 +
 .../tests/src/Functional/EmbedTestBase.php    | 111 +++
 .../src/Functional}/PreviewTest.php           |   9 +-
 .../EmbedButtonAdminTest.php                  | 200 +++++
 .../tests/src/Kernel/IconFileUsageTest.php    |  72 --
 .../embed/tests/src/Kernel/IconTest.php       |  90 ++
 .../entity_embed/.travis-before-script.sh     |  12 -
 web/modules/entity_embed/.travis.yml          | 117 ---
 web/modules/entity_embed/DEVELOPING.md        |   4 -
 web/modules/entity_embed/README.md            |  10 +-
 web/modules/entity_embed/composer.json        |   9 +-
 .../entity_embed/css/entity_embed.dialog.css  |   6 +-
 .../entity_embed/css/entity_embed.editor.css  |  15 +
 .../css/entity_embed.filter.caption.css       |  10 +
 web/modules/entity_embed/entity_embed.api.php |   2 +-
 .../entity_embed/entity_embed.info.yml        |  10 +-
 web/modules/entity_embed/entity_embed.install |  18 +-
 .../entity_embed/entity_embed.libraries.yml   |   8 +
 web/modules/entity_embed/entity_embed.module  | 250 +++++-
 .../entity_embed/entity_embed.routing.yml     |   9 +-
 .../entity_embed/entity_embed.services.yml    |   2 +-
 .../js/plugins/drupalentity/plugin.js         | 385 ++++++--
 .../src/Annotation/EntityEmbedDisplay.php     |  14 +-
 .../src/Controller/PreviewController.php      |  89 ++
 .../src/Entity/EntityEmbedFakeEntity.php      |  19 +
 .../entity_embed/src/EntityEmbedBuilder.php   |  39 +-
 .../EntityEmbedDisplayBase.php                |  41 +-
 .../EntityEmbedDisplayInterface.php           |   5 +-
 .../EntityEmbedDisplayManager.php             |  85 +-
 .../FieldFormatterEntityEmbedDisplayBase.php  |  26 +-
 .../src/Form/EntityEmbedDialog.php            | 443 ++++-----
 .../Plugin/CKEditorPlugin/DrupalEntity.php    |  28 +-
 .../Derivative/FieldFormatterDeriver.php      |  21 +-
 .../src/Plugin/Derivative/ViewModeDeriver.php |  16 +-
 .../src/Plugin/EmbedType/Entity.php           |  56 +-
 .../src/Plugin/Filter/EntityEmbedFilter.php   | 108 ++-
 .../EntityReferenceFieldFormatter.php         | 175 +++-
 .../EntityEmbedDisplay/FileFieldFormatter.php |   6 +-
 .../ImageFieldFormatter.php                   |  53 +-
 .../MediaImageDecorator.php                   | 258 ++++++
 .../ViewModeFieldFormatter.php                |  24 +
 .../src/Tests/EntityEmbedDialogTest.php       | 172 ----
 .../src/Tests/EntityEmbedFilterTest.php       | 198 ----
 .../src/Twig/EntityEmbedTwigExtension.php     |   6 +-
 .../update/entity_embed.update-hook-test.php  |   1 +
 ...tity_form_display.node.article.default.yml |  42 +
 ....entity_view_display.media.image.embed.yml |  29 +
 ....entity_view_display.media.image.thumb.yml |  28 +
 ...tity_view_display.node.article.default.yml |  27 +
 .../core.entity_view_mode.media.embed.yml     |   9 +
 .../core.entity_view_mode.media.thumb.yml     |   9 +
 .../install/editor.editor.full_html.yml       |  64 ++
 .../embed.button.test_media_entity_embed.yml  |  21 +
 .../config/install/embed.button.test_node.yml |  15 +
 ...ld.field.media.image.field_media_image.yml |  40 +
 .../install/field.field.node.article.body.yml |  21 +
 .../field.storage.media.field_media_image.yml |  32 +
 .../install/filter.format.full_html.yml       |  49 +
 .../config/install/media.type.image.yml       |  12 +
 .../config/install/node.type.article.yml      |  10 +
 .../entity_embed_test.info.yml                |  19 +-
 .../entity_embed_test.module                  |  27 +-
 .../src/EntityEmbedTestTwigController.php     |  24 +-
 .../language.content_settings.media.image.yml |  15 +
 ...language.content_settings.node.article.yml |  15 +
 .../config/install/language.entity.fr.yml     |   8 +
 .../entity_embed_translation_test.info.yml    |  20 +
 .../src/Functional/EntityEmbedDialogTest.php  | 122 +++
 .../EntityEmbedDisplayManagerTest.php         | 178 ++++
 .../EntityEmbedEntityBrowserTest.php          |  30 +-
 .../src/Functional}/EntityEmbedHooksTest.php  |  32 +-
 .../src/Functional}/EntityEmbedTestBase.php   |  41 +-
 .../src/Functional}/EntityEmbedTwigTest.php   |  19 +-
 .../Functional}/EntityEmbedUpdateHookTest.php |  16 +-
 .../EntityReferenceFieldFormatterTest.php     |  76 +-
 .../Functional}/FileFieldFormatterTest.php    |  31 +-
 .../Functional}/ImageFieldFormatterTest.php   |  45 +-
 .../Functional/RecursionProtectionTest.php    |  84 ++
 .../ViewModeFieldFormatterTest.php            |  30 +-
 .../FunctionalJavascript/ButtonAdminTest.php  | 138 +++
 .../CKEditorIntegrationTest.php               | 303 +++++++
 .../ConfigurationUiTest.php                   | 266 ++++++
 .../ContentTranslationTest.php                | 344 +++++++
 .../EntityEmbedDialogTest.php                 | 146 +++
 .../EntityEmbedTestBase.php                   | 102 +++
 .../ImageFieldFormatterTest.php               | 265 ++++++
 .../FunctionalJavascript/MediaImageTest.php   | 846 ++++++++++++++++++
 .../SortableTestTrait.php                     | 127 +++
 ...ityEmbedFilterDisabledIntegrationsTest.php |  65 ++
 .../Kernel/EntityEmbedFilterLegacyTest.php    | 100 +++
 .../Kernel/EntityEmbedFilterOverridesTest.php | 155 ++++
 .../src/Kernel/EntityEmbedFilterTest.php      | 454 ++++++++++
 .../src/Kernel/EntityEmbedFilterTestBase.php  | 192 ++++
 .../EntityEmbedFilterTranslationTest.php      | 119 +++
 121 files changed, 7343 insertions(+), 1775 deletions(-)
 delete mode 100644 web/modules/embed/composer.json
 create mode 100644 web/modules/embed/embed.install
 create mode 100644 web/modules/embed/embed.post_update.php
 delete mode 100644 web/modules/embed/src/Tests/EmbedButtonAdminTest.php
 delete mode 100644 web/modules/embed/src/Tests/EmbedTestBase.php
 rename web/modules/embed/{src/Tests => tests/src/Functional}/EmbedButtonEditorAccessCheckTest.php (94%)
 create mode 100644 web/modules/embed/tests/src/Functional/EmbedTestBase.php
 rename web/modules/embed/{src/Tests => tests/src/Functional}/PreviewTest.php (90%)
 create mode 100644 web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php
 delete mode 100644 web/modules/embed/tests/src/Kernel/IconFileUsageTest.php
 create mode 100644 web/modules/embed/tests/src/Kernel/IconTest.php
 delete mode 100644 web/modules/entity_embed/.travis-before-script.sh
 delete mode 100644 web/modules/entity_embed/.travis.yml
 delete mode 100644 web/modules/entity_embed/DEVELOPING.md
 create mode 100644 web/modules/entity_embed/css/entity_embed.editor.css
 create mode 100644 web/modules/entity_embed/css/entity_embed.filter.caption.css
 create mode 100644 web/modules/entity_embed/src/Controller/PreviewController.php
 create mode 100644 web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php
 create mode 100644 web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php
 delete mode 100644 web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php
 delete mode 100644 web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml
 create mode 100644 web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml
 create mode 100644 web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php
 create mode 100644 web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedEntityBrowserTest.php (58%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedHooksTest.php (70%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedTestBase.php (71%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedTwigTest.php (74%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityEmbedUpdateHookTest.php (76%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/EntityReferenceFieldFormatterTest.php (68%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/FileFieldFormatterTest.php (70%)
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/ImageFieldFormatterTest.php (69%)
 create mode 100644 web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php
 rename web/modules/entity_embed/{src/Tests => tests/src/Functional}/ViewModeFieldFormatterTest.php (59%)
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php
 create mode 100644 web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php
 create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php
 create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php
 create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php
 create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php
 create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php
 create mode 100644 web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php

diff --git a/composer.json b/composer.json
index aabb08e27f..afd0960ce2 100644
--- a/composer.json
+++ b/composer.json
@@ -112,11 +112,11 @@
         "drupal/draggableviews": "1.0",
         "drupal/dropzonejs": "2.1",
         "drupal/editor_advanced_link": "1.8",
-        "drupal/embed": "1.0",
+        "drupal/embed": "1.4",
         "drupal/entity": "1.0-beta1",
         "drupal/entity_browser": "1.10",
         "drupal/entity_clone": "1.0.0-beta4",
-        "drupal/entity_embed": "1.0-beta2",
+        "drupal/entity_embed": "1.1",
         "drupal/entity_reference_revisions": "1.8",
         "drupal/externalauth": "1.1",
         "drupal/features": "3.8",
@@ -285,10 +285,6 @@
             "drupal/entity_clone": {
                 "3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch"
             },
-            "drupal/entity_embed": {
-                "2881745": "patches/entity_embed_2881745-22.patch",
-                "2511404": "patches/entity-embed-img-link-2511404-68.patch"
-            },
             "drupal/honeypot": {
                 "2811189": "https://www.drupal.org/files/issues/2019-08-08/honeypot_field_weight_2811189-18.patch"
             },
diff --git a/composer.lock b/composer.lock
index cd907bc32a..3c8b50852b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "7d3787aaafd1ba3db8d0785fee363f8b",
+    "content-hash": "660ffeb38362eee6436d2cb9a6cf8c4a",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -4199,29 +4199,26 @@
         },
         {
             "name": "drupal/embed",
-            "version": "1.0.0",
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/embed.git",
-                "reference": "8.x-1.0"
+                "reference": "8.x-1.4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.0.zip",
-                "reference": "8.x-1.0",
-                "shasum": "cc746ad807260e01c7788dd82110dcebbb4d678a"
+                "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.4.zip",
+                "reference": "8.x-1.4",
+                "shasum": "09a2bda039bfbb3fff01c91964384bf3d924b8c5"
             },
             "require": {
-                "drupal/core": "~8.0"
+                "drupal/core": "^8.7.7 || ^9"
             },
             "type": "drupal-module",
             "extra": {
-                "branch-alias": {
-                    "dev-1.x": "1.x-dev"
-                },
                 "drupal": {
-                    "version": "8.x-1.0",
-                    "datestamp": "1490755685",
+                    "version": "8.x-1.4",
+                    "datestamp": "1590176831",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -4230,7 +4227,7 @@
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
             "license": [
-                "GPL-2.0+"
+                "GPL-2.0-or-later"
             ],
             "authors": [
                 {
@@ -4249,17 +4246,19 @@
                     "name": "cs_shadow",
                     "homepage": "https://www.drupal.org/user/2828287"
                 },
+                {
+                    "name": "phenaproxima",
+                    "homepage": "https://www.drupal.org/user/205645"
+                },
                 {
                     "name": "slashrsm",
                     "homepage": "https://www.drupal.org/user/744628"
                 }
             ],
-            "description": "Provide a framework for various different types of embeds in WYSIWYG editors, common functionality, interfaces, and standards.",
+            "description": "Provides a framework for different types of embeds in text editors.",
             "homepage": "https://www.drupal.org/project/embed",
             "support": {
-                "source": "http://cgit.drupalcode.org/embed",
-                "issues": "https://www.drupal.org/project/issues/embed",
-                "irc": "irc://irc.freenode.org/drupal-media"
+                "source": "https://git.drupalcode.org/project/embed"
             }
         },
         {
@@ -4538,41 +4537,34 @@
         },
         {
             "name": "drupal/entity_embed",
-            "version": "1.0.0-beta2",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/entity_embed.git",
-                "reference": "8.x-1.0-beta2"
+                "reference": "8.x-1.1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.0-beta2.zip",
-                "reference": "8.x-1.0-beta2",
-                "shasum": "21cdeb2b058efce461683aed9a8951053512dca7"
+                "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.1.zip",
+                "reference": "8.x-1.1",
+                "shasum": "f2c3f4b3071cbd69db94c5255e1db89510995b5d"
             },
             "require": {
-                "drupal/core": "*",
-                "drupal/embed": "*"
+                "drupal/core": "^8.8 || ^9",
+                "drupal/embed": "^1.3"
             },
             "require-dev": {
-                "drupal/entity_browser": "*"
+                "drupal/entity_browser": "^2.2"
             },
             "type": "drupal-module",
             "extra": {
-                "branch-alias": {
-                    "dev-1.x": "1.x-dev"
-                },
                 "drupal": {
-                    "version": "8.x-1.0-beta2",
-                    "datestamp": "1476698339",
+                    "version": "8.x-1.1",
+                    "datestamp": "1585252806",
                     "security-coverage": {
-                        "status": "not-covered",
-                        "message": "Beta releases are not covered by Drupal security advisories."
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
                     }
-                },
-                "patches_applied": {
-                    "2881745": "patches/entity_embed_2881745-22.patch",
-                    "2511404": "patches/entity-embed-img-link-2511404-68.patch"
                 }
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
@@ -4600,6 +4592,10 @@
                     "name": "cs_shadow",
                     "homepage": "https://www.drupal.org/user/2828287"
                 },
+                {
+                    "name": "oknate",
+                    "homepage": "https://www.drupal.org/user/471638"
+                },
                 {
                     "name": "phenaproxima",
                     "homepage": "https://www.drupal.org/user/205645"
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 9d55d91d2d..70eac805f2 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -4324,30 +4324,27 @@
     },
     {
         "name": "drupal/embed",
-        "version": "1.0.0",
-        "version_normalized": "1.0.0.0",
+        "version": "1.4.0",
+        "version_normalized": "1.4.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/embed.git",
-            "reference": "8.x-1.0"
+            "reference": "8.x-1.4"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.0.zip",
-            "reference": "8.x-1.0",
-            "shasum": "cc746ad807260e01c7788dd82110dcebbb4d678a"
+            "url": "https://ftp.drupal.org/files/projects/embed-8.x-1.4.zip",
+            "reference": "8.x-1.4",
+            "shasum": "09a2bda039bfbb3fff01c91964384bf3d924b8c5"
         },
         "require": {
-            "drupal/core": "~8.0"
+            "drupal/core": "^8.7.7 || ^9"
         },
         "type": "drupal-module",
         "extra": {
-            "branch-alias": {
-                "dev-1.x": "1.x-dev"
-            },
             "drupal": {
-                "version": "8.x-1.0",
-                "datestamp": "1490755685",
+                "version": "8.x-1.4",
+                "datestamp": "1590176831",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -4357,7 +4354,7 @@
         "installation-source": "dist",
         "notification-url": "https://packages.drupal.org/8/downloads",
         "license": [
-            "GPL-2.0+"
+            "GPL-2.0-or-later"
         ],
         "authors": [
             {
@@ -4376,17 +4373,19 @@
                 "name": "cs_shadow",
                 "homepage": "https://www.drupal.org/user/2828287"
             },
+            {
+                "name": "phenaproxima",
+                "homepage": "https://www.drupal.org/user/205645"
+            },
             {
                 "name": "slashrsm",
                 "homepage": "https://www.drupal.org/user/744628"
             }
         ],
-        "description": "Provide a framework for various different types of embeds in WYSIWYG editors, common functionality, interfaces, and standards.",
+        "description": "Provides a framework for different types of embeds in text editors.",
         "homepage": "https://www.drupal.org/project/embed",
         "support": {
-            "source": "http://cgit.drupalcode.org/embed",
-            "issues": "https://www.drupal.org/project/issues/embed",
-            "irc": "irc://irc.freenode.org/drupal-media"
+            "source": "https://git.drupalcode.org/project/embed"
         }
     },
     {
@@ -4672,42 +4671,35 @@
     },
     {
         "name": "drupal/entity_embed",
-        "version": "1.0.0-beta2",
-        "version_normalized": "1.0.0.0-beta2",
+        "version": "1.1.0",
+        "version_normalized": "1.1.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/entity_embed.git",
-            "reference": "8.x-1.0-beta2"
+            "reference": "8.x-1.1"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.0-beta2.zip",
-            "reference": "8.x-1.0-beta2",
-            "shasum": "21cdeb2b058efce461683aed9a8951053512dca7"
+            "url": "https://ftp.drupal.org/files/projects/entity_embed-8.x-1.1.zip",
+            "reference": "8.x-1.1",
+            "shasum": "f2c3f4b3071cbd69db94c5255e1db89510995b5d"
         },
         "require": {
-            "drupal/core": "*",
-            "drupal/embed": "*"
+            "drupal/core": "^8.8 || ^9",
+            "drupal/embed": "^1.3"
         },
         "require-dev": {
-            "drupal/entity_browser": "*"
+            "drupal/entity_browser": "^2.2"
         },
         "type": "drupal-module",
         "extra": {
-            "branch-alias": {
-                "dev-1.x": "1.x-dev"
-            },
             "drupal": {
-                "version": "8.x-1.0-beta2",
-                "datestamp": "1476698339",
+                "version": "8.x-1.1",
+                "datestamp": "1585252806",
                 "security-coverage": {
-                    "status": "not-covered",
-                    "message": "Beta releases are not covered by Drupal security advisories."
+                    "status": "covered",
+                    "message": "Covered by Drupal's security advisory policy"
                 }
-            },
-            "patches_applied": {
-                "2881745": "patches/entity_embed_2881745-22.patch",
-                "2511404": "patches/entity-embed-img-link-2511404-68.patch"
             }
         },
         "installation-source": "dist",
@@ -4736,6 +4728,10 @@
                 "name": "cs_shadow",
                 "homepage": "https://www.drupal.org/user/2828287"
             },
+            {
+                "name": "oknate",
+                "homepage": "https://www.drupal.org/user/471638"
+            },
             {
                 "name": "phenaproxima",
                 "homepage": "https://www.drupal.org/user/205645"
diff --git a/web/modules/embed/README.md b/web/modules/embed/README.md
index e2039e148e..e2b97ae27b 100644
--- a/web/modules/embed/README.md
+++ b/web/modules/embed/README.md
@@ -1,5 +1,3 @@
 # Embed Module
 
-[![Travis build status](https://img.shields.io/travis/drupal-media/embed/8.x-1.x.svg)](https://travis-ci.org/drupal-media/embed) [![Scrutinizer code quality](https://img.shields.io/scrutinizer/g/drupal-media/embed/8.x-1.x.svg)](https://scrutinizer-ci.com/g/drupal-media/embed)
-
 Provides an API for embedding and a UI for creating embed buttons.
diff --git a/web/modules/embed/composer.json b/web/modules/embed/composer.json
deleted file mode 100644
index d0003e3988..0000000000
--- a/web/modules/embed/composer.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "name": "drupal/embed",
-  "description": "Provide a framework for various different types of embeds in WYSIWYG editors, common functionality, interfaces, and standards.",
-  "type": "drupal-module",
-  "homepage": "https://www.drupal.org/project/embed",
-  "support": {
-    "issues": "https://www.drupal.org/project/issues/embed",
-    "irc": "irc://irc.freenode.org/drupal-media"
-  },
-  "license": "GPL-2.0+"
-}
diff --git a/web/modules/embed/config/schema/embed.schema.yml b/web/modules/embed/config/schema/embed.schema.yml
index e232a20d92..545262d9ce 100644
--- a/web/modules/embed/config/schema/embed.schema.yml
+++ b/web/modules/embed/config/schema/embed.schema.yml
@@ -26,9 +26,19 @@ embed.button.*:
       label: 'Embed type plugin ID'
     type_settings:
       type: embed.embed_type_settings.[%parent.type_id]
+    icon:
+      type: mapping
+      label: 'Image icon data'
+      mapping:
+        uri:
+          type: string
+          label: 'Image file URI'
+        data:
+          type: string
+          label: 'Base-64 encoded image contents'
     icon_uuid:
       type: string
-      label: 'Button icon UUID'
+      label: 'Deprecated icon image file entity UUID'
 
 embed.embed_type_settings.*:
   type: mapping
diff --git a/web/modules/embed/embed.info.yml b/web/modules/embed/embed.info.yml
index 3e2cea64b5..5a71075732 100644
--- a/web/modules/embed/embed.info.yml
+++ b/web/modules/embed/embed.info.yml
@@ -1,11 +1,10 @@
 name: Embed
 type: module
 description: 'Provides a framework for different types of embeds in text editors.'
-# core: 8.x
+core_version_requirement: ^8.7.7 || ^9
 configure: entity.embed_button.collection
 
-# Information added by Drupal.org packaging script on 2017-03-29
-version: '8.x-1.0'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-05-22
+version: '8.x-1.4'
 project: 'embed'
-datestamp: 1490755690
+datestamp: 1590176834
diff --git a/web/modules/embed/embed.install b/web/modules/embed/embed.install
new file mode 100644
index 0000000000..9f5ed97bfe
--- /dev/null
+++ b/web/modules/embed/embed.install
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the embed module.
+ */
+
+/**
+ * Moved to embed_post_update_convert_encoded_icon_data().
+ */
+function embed_update_8101() {
+  // This update function has been moved to
+  // embed_post_update_convert_encoded_icon_data().
+}
diff --git a/web/modules/embed/embed.module b/web/modules/embed/embed.module
index 77349e7f34..2834b39c6a 100644
--- a/web/modules/embed/embed.module
+++ b/web/modules/embed/embed.module
@@ -1,6 +1,8 @@
 <?php
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Implements hook_form_FORM_ID_alter() on behalf of ckeditor.module.
@@ -31,3 +33,82 @@ function ckeditor_form_embed_button_add_form_validate(array &$form, FormStateInt
     $form_state->setErrorByName('id', t('A CKEditor button with ID %id already exists.', ['%id' => $button_id]));
   }
 }
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function embed_form_filter_format_edit_form_alter(array &$form) {
+  $form['#validate'][] = 'embed_filter_format_edit_form_validate';
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function embed_form_filter_format_add_form_alter(array &$form) {
+  $form['#validate'][] = 'embed_filter_format_edit_form_validate';
+}
+
+/**
+ * Validate callback for buttons that have a required_filter_plugin_id.
+ */
+function embed_filter_format_edit_form_validate($form, FormStateInterface $form_state) {
+  if ($form_state->getTriggeringElement()['#name'] !== 'op') {
+    return;
+  }
+
+  // Right now we can only validate CKEditor configurations.
+  if ($form_state->getValue(['editor', 'editor']) !== 'ckeditor') {
+    return;
+  }
+
+  $button_group_path = [
+    'editor',
+    'settings',
+    'toolbar',
+    'button_groups',
+  ];
+
+  $buttons_to_validate = [];
+  $buttons = \Drupal::service('plugin.manager.ckeditor.plugin')->getButtons();
+  foreach ($buttons as $plugin_id => $plugin_buttons) {
+    foreach ($plugin_buttons as $button_id => $button) {
+      if (!empty($button['required_filter_plugin_id'])) {
+        $buttons_to_validate[$plugin_id . ':' . $button_id] = $button['required_filter_plugin_id'];
+      }
+    }
+  }
+
+  if ($button_groups = $form_state->getValue($button_group_path)) {
+    $selected_buttons = [];
+    $button_groups = Json::decode($button_groups);
+
+    foreach ($button_groups as $button_row) {
+      foreach ($button_row as $button_group) {
+        $selected_buttons = array_merge($selected_buttons, array_values($button_group['items']));
+      }
+    }
+
+    $get_filter_label = function ($filter_plugin_id) use ($form) {
+      return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup'];
+    };
+
+    foreach ($buttons_to_validate as $button_id => $filter_plugin_id) {
+      list($plugin_id, $button_id) = explode(':', $button_id, 2);
+      if (in_array($button_id, $selected_buttons, TRUE)) {
+        $filter_enabled = $form_state->getValue([
+          'filters',
+          $filter_plugin_id,
+          'status',
+        ]);
+
+        if (!$filter_enabled) {
+          $error_message = new TranslatableMarkup('The %filter-label filter must be enabled to use the %button button.', [
+            '%filter-label' => $get_filter_label($filter_plugin_id),
+            '%button' => $buttons[$plugin_id][$button_id]['label'],
+          ]);
+          $form_state->setErrorByName('filters][' . $filter_plugin_id . '][status', $error_message);
+        }
+      }
+    }
+  }
+}
diff --git a/web/modules/embed/embed.post_update.php b/web/modules/embed/embed.post_update.php
new file mode 100644
index 0000000000..42a3e3640b
--- /dev/null
+++ b/web/modules/embed/embed.post_update.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Post update functions for Embed.
+ */
+
+/**
+ * Convert embed button icons from managed files to encoded data.
+ */
+function embed_post_update_convert_encoded_icon_data() {
+  $file_storage = \Drupal::entityTypeManager()->getStorage('file');
+  $file_usage = \Drupal::service('file.usage');
+
+  /** @var \Drupal\embed\EmbedButtonInterface[] $buttons */
+  $buttons = \Drupal::entityTypeManager()->getStorage('embed_button')->loadMultiple();
+
+  foreach ($buttons as $button) {
+    if ($icon_uuid = $button->get('icon_uuid')) {
+      if (!$button->get('icon')) {
+        // Read in the button icon file and convert to base 64 encoded string.
+        if ($files = $file_storage->loadByProperties(['uuid' => $icon_uuid])) {
+          $file = reset($files);
+          $button->set('icon', $button::convertImageToEncodedData($file->getFileUri()));
+
+          // Decrement file usage for this embed button.
+          $file_usage->delete($file, 'embed', 'embed_button', $button->id());
+        }
+        else {
+          \Drupal::logger('embed')->warning('The embed button @label had an uploaded icon image file with UUID @uuid, but it no longer exists in the database. It has been reverted back to the default button icon and you may wish to re-upload a new version.', [
+            '@label' => $button->label(),
+            '@uuid' => $icon_uuid,
+          ]);
+        }
+      }
+
+      $button->set('icon_uuid', NULL);
+      $button->save();
+    }
+  }
+}
diff --git a/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php b/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php
index 543d498624..70fc5f4aea 100644
--- a/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php
+++ b/web/modules/embed/src/Access/EmbedButtonEditorAccessCheck.php
@@ -21,7 +21,7 @@ class EmbedButtonEditorAccessCheck implements AccessInterface {
    * @code
    * pattern: '/foo/{editor}/{embed_button}'
    * requirements:
-   *   _embed_button_filter_access: 'TRUE'
+   *   _embed_button_editor_access: 'TRUE'
    * @endcode
    *
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
diff --git a/web/modules/embed/src/Controller/EmbedController.php b/web/modules/embed/src/Controller/EmbedController.php
index 65df04fc9b..42c6009368 100644
--- a/web/modules/embed/src/Controller/EmbedController.php
+++ b/web/modules/embed/src/Controller/EmbedController.php
@@ -2,13 +2,18 @@
 
 namespace Drupal\embed\Controller;
 
+use Drupal\Core\Ajax\AjaxHelperTrait;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\editor\EditorInterface;
 use Drupal\embed\Ajax\EmbedInsertCommand;
 use Drupal\embed\EmbedButtonInterface;
 use Drupal\filter\FilterFormatInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
@@ -16,6 +21,34 @@
  */
 class EmbedController extends ControllerBase {
 
+  use AjaxHelperTrait;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs an EmbedController instance.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(RendererInterface $renderer) {
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('renderer')
+    );
+  }
+
   /**
    * Returns an Ajax response to generate preview of embedded items.
    *
@@ -33,8 +66,8 @@ class EmbedController extends ControllerBase {
    *   The preview of the embedded item specified by the data attributes.
    */
   public function preview(Request $request, FilterFormatInterface $filter_format) {
-    $text = $request->get('value');
-    if ($text == '') {
+    $text = $request->get('text') ?: $request->get('value');
+    if (empty($text)) {
       throw new NotFoundHttpException();
     }
 
@@ -42,11 +75,27 @@ public function preview(Request $request, FilterFormatInterface $filter_format)
       '#type' => 'processed_text',
       '#text' => $text,
       '#format' => $filter_format->id(),
+      '#langcode' => $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(),
     ];
 
-    $response = new AjaxResponse();
-    $response->addCommand(new EmbedInsertCommand($build));
-    return $response;
+    if ($this->isAjax()) {
+      $response = new AjaxResponse();
+      $response->addCommand(new EmbedInsertCommand($build));
+      return $response;
+    }
+    else {
+      $html = $this->renderer->renderPlain($build);
+      // Note that we intentionally do not use:
+      // - \Drupal\Core\Cache\CacheableResponse because caching it on the server
+      //   side is wasteful, hence there is no need for cacheability metadata.
+      // - \Drupal\Core\Render\HtmlResponse because there is no need for
+      //   attachments nor cacheability metadata.
+      return (new Response($html))
+        // Do not allow any intermediary to cache the response, only end user.
+        ->setPrivate()
+        // Allow the end user to cache it for up to 5 minutes.
+        ->setMaxAge(300);
+    }
   }
 
   /**
diff --git a/web/modules/embed/src/EmbedButtonInterface.php b/web/modules/embed/src/EmbedButtonInterface.php
index 6a2d5708fe..907c213e81 100644
--- a/web/modules/embed/src/EmbedButtonInterface.php
+++ b/web/modules/embed/src/EmbedButtonInterface.php
@@ -59,6 +59,9 @@ public function getTypeSettings();
    *
    * @return \Drupal\file\FileInterface
    *   The file entity of the button icon.
+   *
+   * @deprecated in embed:1.2 and will be removed in embed:1.3. Use
+   *   \Drupal\embed\EmbedButtonInterface::getIconUrl() instead.
    */
   public function getIconFile();
 
@@ -73,4 +76,30 @@ public function getIconFile();
    */
   public function getIconUrl();
 
+  /**
+   * Convert a file on the filesystem to encoded data.
+   *
+   * @param string $uri
+   *   An image file URI.
+   *
+   * @return array
+   *   An array of data about the encoded image including:
+   *     - uri: The URI of the file.
+   *     - data: The base-64 encoded contents of the file.
+   */
+  public static function convertImageToEncodedData($uri);
+
+  /**
+   * Convert image encoded data to a file on the filesystem.
+   *
+   * @param array $data
+   *   An array of data about the encoded image including:
+   *     - uri: The URI of the file.
+   *     - data: The base-64 encoded contents of the file.
+   *
+   * @return string
+   *   An image file URI.
+   */
+  public static function convertEncodedDataToImage(array $data);
+
 }
diff --git a/web/modules/embed/src/EmbedCKEditorPluginBase.php b/web/modules/embed/src/EmbedCKEditorPluginBase.php
index 5ebbe7ac47..64867f0c61 100644
--- a/web/modules/embed/src/EmbedCKEditorPluginBase.php
+++ b/web/modules/embed/src/EmbedCKEditorPluginBase.php
@@ -2,14 +2,17 @@
 
 namespace Drupal\embed;
 
+use Drupal\ckeditor\CKEditorPluginBase;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\ckeditor\CKEditorPluginBase;
 use Drupal\editor\Entity\Editor;
 use Drupal\embed\Entity\EmbedButton;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
+/**
+ * Provides a base class for embed CKEditor plugins.
+ */
 abstract class EmbedCKEditorPluginBase extends CKEditorPluginBase implements ContainerFactoryPluginInterface {
 
   /**
@@ -47,7 +50,7 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('entity.query')->get('embed_button')
+      $container->get('entity_type.manager')->getStorage('embed_button')->getQuery()
     );
   }
 
@@ -67,13 +70,27 @@ public function getButtons() {
     return $buttons;
   }
 
+  /**
+   * Build the information about the specific button.
+   *
+   * @param \Drupal\embed\EmbedButtonInterface $embed_button
+   *   The embed button.
+   *
+   * @return array
+   *   The array for use with getButtons().
+   */
   protected function getButton(EmbedButtonInterface $embed_button) {
-    return [
+    $info = [
       'id' => $embed_button->id(),
       'name' => Html::escape($embed_button->label()),
       'label' => Html::escape($embed_button->label()),
       'image' => $embed_button->getIconUrl(),
     ];
+    $definition = $this->getPluginDefinition();
+    if (!empty($definition['required_filter_plugin_id'])) {
+      $info['required_filter_plugin_id'] = $definition['required_filter_plugin_id'];
+    }
+    return $info;
   }
 
   /**
diff --git a/web/modules/embed/src/EmbedType/EmbedTypeInterface.php b/web/modules/embed/src/EmbedType/EmbedTypeInterface.php
index b6d3adfdf8..9e33882e68 100644
--- a/web/modules/embed/src/EmbedType/EmbedTypeInterface.php
+++ b/web/modules/embed/src/EmbedType/EmbedTypeInterface.php
@@ -2,8 +2,9 @@
 
 namespace Drupal\embed\EmbedType;
 
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\DependentPluginInterface;
 use Drupal\Component\Plugin\PluginInspectionInterface;
-use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
 
 /**
@@ -11,7 +12,7 @@
  *
  * @ingroup embed_api
  */
-interface EmbedTypeInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface {
+interface EmbedTypeInterface extends ConfigurableInterface, DependentPluginInterface, PluginFormInterface, PluginInspectionInterface {
 
   /**
    * Gets a configuration value.
diff --git a/web/modules/embed/src/EmbedType/EmbedTypeManager.php b/web/modules/embed/src/EmbedType/EmbedTypeManager.php
index 8c2a5d5443..aaf4102fa0 100644
--- a/web/modules/embed/src/EmbedType/EmbedTypeManager.php
+++ b/web/modules/embed/src/EmbedType/EmbedTypeManager.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\embed\Annotation\EmbedType;
 
 /**
  * Provides an Embed type plugin manager.
@@ -27,7 +28,7 @@ class EmbedTypeManager extends DefaultPluginManager {
    *   The module handler to invoke the alter hook with.
    */
   public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
-    parent::__construct('Plugin/EmbedType', $namespaces, $module_handler, 'Drupal\embed\EmbedType\EmbedTypeInterface', 'Drupal\embed\Annotation\EmbedType');
+    parent::__construct('Plugin/EmbedType', $namespaces, $module_handler, EmbedTypeInterface::class, EmbedType::class);
     $this->alterInfo('embed_type_plugins');
     $this->setCacheBackend($cache_backend, 'embed_type_plugins');
   }
diff --git a/web/modules/embed/src/Entity/EmbedButton.php b/web/modules/embed/src/Entity/EmbedButton.php
index 9188b95b5a..7a653095ff 100644
--- a/web/modules/embed/src/Entity/EmbedButton.php
+++ b/web/modules/embed/src/Entity/EmbedButton.php
@@ -2,8 +2,9 @@
 
 namespace Drupal\embed\Entity;
 
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\embed\EmbedButtonInterface;
 
@@ -37,6 +38,7 @@
  *     "id",
  *     "type_id",
  *     "type_settings",
+ *     "icon",
  *     "icon_uuid",
  *   }
  * )
@@ -76,11 +78,11 @@ class EmbedButton extends ConfigEntityBase implements EmbedButtonInterface {
   public $type_settings = [];
 
   /**
-   * UUID of the button's icon file.
+   * An array of data about the encoded button image.
    *
-   * @var string
+   * @var array
    */
-  public $icon_uuid;
+  public $icon = [];
 
   /**
    * {@inheritdoc}
@@ -96,9 +98,7 @@ public function getTypeLabel() {
     if ($definition = $this->embedTypeManager()->getDefinition($this->getTypeId(), FALSE)) {
       return $definition['label'];
     }
-    else {
-      return $this->t('Unknown');
-    }
+    return $this->t('Unknown');
   }
 
   /**
@@ -114,8 +114,10 @@ public function getTypePlugin() {
    * {@inheritdoc}
    */
   public function getIconFile() {
-    if ($this->icon_uuid) {
-      return $this->entityManager()->loadEntityByUuid('file', $this->icon_uuid);
+    @trigger_error(__METHOD__ . ' is deprecated in Embed 1.2 and will be removed before 1.3.', E_USER_DEPRECATED);
+    if (!empty($this->icon_uuid)) {
+      $files = $this->entityTypeManager()->getStorage('file')->loadByProperties(['uuid' => $this->icon_uuid]);
+      return reset($files);
     }
   }
 
@@ -123,12 +125,18 @@ public function getIconFile() {
    * {@inheritdoc}
    */
   public function getIconUrl() {
-    if ($image = $this->getIconFile()) {
-      return file_create_url($image->getFileUri());
+    if (!empty($this->icon)) {
+      $uri = $this->icon['uri'];
+      if (!is_file($uri) && !UrlHelper::isExternal($uri)) {
+        static::convertEncodedDataToImage($this->icon);
+      }
+      $uri = file_create_url($uri);
     }
     else {
-      return $this->getTypePlugin()->getDefaultIconUrl();
+      $uri = $this->getTypePlugin()->getDefaultIconUrl();
     }
+
+    return file_url_transform_relative($uri);
   }
 
   /**
@@ -137,16 +145,12 @@ public function getIconUrl() {
   public function calculateDependencies() {
     parent::calculateDependencies();
 
-    // Add the file icon entity as dependency if an UUID was specified.
-    if ($this->icon_uuid && $file_icon = $this->entityManager()->loadEntityByUuid('file', $this->icon_uuid)) {
-      $this->addDependency($file_icon->getConfigDependencyKey(), $file_icon->getConfigDependencyName());
-    }
-
     // Gather the dependencies of the embed type plugin.
-    $plugin = $this->getTypePlugin();
-    $this->calculatePluginDependencies($plugin);
-
-    return $this->dependencies;
+    if ($plugin = $this->getTypePlugin()) {
+      $this->calculatePluginDependencies($plugin);
+      return $this->dependencies;
+    }
+    return NULL;
   }
 
   /**
@@ -159,70 +163,45 @@ protected function embedTypeManager() {
     return \Drupal::service('plugin.manager.embed.type');
   }
 
-  /**
-   * Gets the file usage service.
-   *
-   * @return \Drupal\file\FileUsage\FileUsageInterface
-   *   The file usage service.
-   */
-  protected function fileUsage() {
-    return \Drupal::service('file.usage');
-  }
-
   /**
    * {@inheritdoc}
    */
-  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
-    parent::postSave($storage, $update);
-
-    $icon_file = $this->getIconFile();
-    if (isset($this->original) && $old_icon_file = $this->original->getIconFile()) {
-      /** @var \Drupal\file\FileInterface $old_icon_file */
-      if (!$icon_file || $icon_file->uuid() != $old_icon_file->uuid()) {
-        $this->fileUsage()->delete($old_icon_file, 'embed', $this->getEntityTypeId(), $this->id());
-      }
-    }
-
-    if ($icon_file) {
-      $usage = $this->fileUsage()->listUsage($icon_file);
-      if (empty($usage['embed'][$this->getEntityTypeId()][$this->id()])) {
-        $this->fileUsage()->add($icon_file, 'embed', $this->getEntityTypeId(), $this->id());
-      }
+  public function getTypeSetting($key, $default = NULL) {
+    if (isset($this->type_settings[$key])) {
+      return $this->type_settings[$key];
     }
+    return $default;
   }
 
   /**
    * {@inheritdoc}
    */
-  public static function postDelete(EntityStorageInterface $storage, array $entities) {
-    parent::postDelete($storage, $entities);
-
-    // Remove file usage for any button icons.
-    foreach ($entities as $entity) {
-      /** @var \Drupal\embed\EmbedButtonInterface $entity */
-      if ($icon_file = $entity->getIconFile()) {
-        \Drupal::service('file.usage')->delete($icon_file, 'embed', $entity->getEntityTypeId(), $entity->id());
-      }
-    }
+  public function getTypeSettings() {
+    return $this->type_settings;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getTypeSetting($key, $default = NULL) {
-    if (isset($this->type_settings[$key])) {
-      return $this->type_settings[$key];
-    }
-    else {
-      return $default;
-    }
+  public static function convertImageToEncodedData($uri) {
+    return [
+      'data' => base64_encode(file_get_contents($uri)),
+      'uri' => $uri,
+    ];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getTypeSettings() {
-    return $this->type_settings;
+  public static function convertEncodedDataToImage(array $data) {
+    if (!is_file($data['uri'])) {
+      $directory = dirname($data['uri']);
+      /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
+      $fileSystem = \Drupal::service('file_system');
+      $fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
+      $fileSystem->saveData(base64_decode($data['data']), $data['uri'], FileSystemInterface::EXISTS_REPLACE);
+    }
+    return $data['uri'];
   }
 
 }
diff --git a/web/modules/embed/src/Form/EmbedButtonForm.php b/web/modules/embed/src/Form/EmbedButtonForm.php
index 2702e22f5c..5379c90f37 100644
--- a/web/modules/embed/src/Form/EmbedButtonForm.php
+++ b/web/modules/embed/src/Form/EmbedButtonForm.php
@@ -7,10 +7,11 @@
 use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityForm;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\embed\EmbedType\EmbedTypeManager;
+use Drupal\embed\Entity\EmbedButton;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -84,7 +85,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
       '#disabled' => !$button->isNew(),
       '#machine_name' => [
-        'exists' => ['Drupal\embed\Entity\EmbedButton', 'load'],
+        'exists' => [EmbedButton::class, 'load'],
       ],
       '#description' => $this->t('A unique machine-readable name for this embed button. It must only contain lowercase letters, numbers, and underscores.'),
     ];
@@ -101,8 +102,8 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#disabled' => !$button->isNew(),
     ];
-    if (count($form['type_id']['#options']) == 0) {
-      drupal_set_message($this->t('No embed types found.'), 'warning');
+    if (empty($form['type_id']['#options'])) {
+      $this->messenger()->addWarning($this->t('No embed types found.'));
     }
 
     // Add the embed type plugin settings.
@@ -119,7 +120,7 @@ public function form(array $form, FormStateInterface $form_state) {
       }
     }
     catch (PluginNotFoundException $exception) {
-      drupal_set_message($exception->getMessage(), 'error');
+      $this->messenger()->addError($exception->getMessage());
       watchdog_exception('embed', $exception);
       $form['type_id']['#disabled'] = FALSE;
     }
@@ -127,17 +128,38 @@ public function form(array $form, FormStateInterface $form_state) {
     $config = $this->config('embed.settings');
     $upload_location = $config->get('file_scheme') . '://' . $config->get('upload_directory') . '/';
     $form['icon_file'] = [
-      '#title' => $this->t('Button icon'),
       '#type' => 'managed_file',
-      '#description' => $this->t('Icon for the button to be shown in CKEditor toolbar. Leave empty to use the default Entity icon.'),
+      '#title' => $this->t('Button icon'),
       '#upload_location' => $upload_location,
       '#upload_validators' => [
-        'file_validate_extensions' => ['gif png jpg jpeg'],
+        'file_validate_extensions' => ['gif png jpg jpeg svg'],
         'file_validate_image_resolution' => ['32x32', '16x16'],
       ],
     ];
-    if ($file = $button->getIconFile()) {
-      $form['icon_file']['#default_value'] = ['target_id' => $file->id()];
+
+    if (!$button->isNew()) {
+      $form['icon_reset'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Reset to default icon'),
+        '#access' => $button->getIconUrl() !== $button->getTypePlugin()->getDefaultIconUrl(),
+      ];
+
+      $form['icon_preview'] = [
+        '#type' => 'fieldset',
+        '#title' => $this->t('Current icon preview'),
+      ];
+      $form['icon_preview']['image'] = [
+        '#theme' => 'image',
+        '#uri' => $button->getIconUrl(),
+        '#alt' => $this->t('Preview of @label button icon', ['@label' => $button->label()]),
+      ];
+
+      // Show an even nicer preview with CKEditor being used.
+      if ($this->moduleHandler->moduleExists('ckeditor')) {
+        $form['icon_preview']['image']['#prefix'] = '<div data-toolbar="active" role="form" class="ckeditor-toolbar ckeditor-toolbar-active clearfix"><ul class="ckeditor-active-toolbar-configuration" role="presentation" aria-label="CKEditor toolbar and button configuration."><li class="ckeditor-row" role="group" aria-labelledby="ckeditor-active-toolbar"><ul class="ckeditor-toolbar-groups clearfix js-sortable"><li class="ckeditor-toolbar-group" role="presentation" data-drupal-ckeditor-type="group" data-drupal-ckeditor-toolbar-group-name="Embed button icon preview" tabindex="0"><h3 class="ckeditor-toolbar-group-name" id="ckeditor-toolbar-group-aria-label-for-formatting">Embed button icon preview</h3><ul class="ckeditor-buttons ckeditor-toolbar-group-buttons js-sortable" role="toolbar" data-drupal-ckeditor-button-sorting="target" aria-labelledby="ckeditor-toolbar-group-aria-label-for-formatting"><li data-drupal-ckeditor-button-name="Bold" class="ckeditor-button"><a href="#" role="button" title="' . $button->label() . '" aria-label="' . $button->label() . '"><span class="cke_button_icon">';
+        $form['icon_preview']['image']['#suffix'] = '</span></a></li></ul></li></ul></div>';
+        $form['icon_preview']['#attached']['library'][] = 'ckeditor/drupal.ckeditor.admin';
+      }
     }
 
     return $form;
@@ -181,30 +203,32 @@ public function save(array $form, FormStateInterface $form_state) {
     $form_state->setValue('type_settings', $plugin->getConfiguration());
     $button->set('type_settings', $plugin->getConfiguration());
 
+    // If a file was uploaded to be used as the icon, get an encoded URL to be
+    // stored in the config entity.
     $icon_fid = $form_state->getValue(['icon_file', '0']);
-    // If a file was uploaded to be used as the icon, get its UUID to be stored
-    // in the config entity.
     if (!empty($icon_fid) && $file = $this->entityTypeManager->getStorage('file')->load($icon_fid)) {
-      $button->set('icon_uuid', $file->uuid());
+      $file->setPermanent();
+      $file->save();
+      $button->set('icon', EmbedButton::convertImageToEncodedData($file->getFileUri()));
     }
-    else {
-      $button->set('icon_uuid', NULL);
+    elseif ($form_state->getValue('icon_reset')) {
+      $button->set('icon', NULL);
     }
 
     $status = $button->save();
 
     $t_args = ['%label' => $button->label()];
 
-    if ($status == SAVED_UPDATED) {
-      drupal_set_message($this->t('The embed button %label has been updated.', $t_args));
+    if ($status === SAVED_UPDATED) {
+      $this->messenger()->addStatus($this->t('The embed button %label has been updated.', $t_args));
+      $this->logger('embed')->info('Updated embed button %label.', $t_args);
     }
-    elseif ($status == SAVED_NEW) {
-      drupal_set_message($this->t('The embed button %label has been added.', $t_args));
-      $context = array_merge($t_args, ['link' => $button->link($this->t('View'), 'collection')]);
-      $this->logger('embed')->notice('Added embed button %label.', $context);
+    elseif ($status === SAVED_NEW) {
+      $this->messenger()->addStatus($this->t('The embed button %label has been added.', $t_args));
+      $this->logger('embed')->info('Added embed button %label.', $t_args);
     }
 
-    $form_state->setRedirectUrl($button->urlInfo('collection'));
+    $form_state->setRedirectUrl($button->toUrl());
   }
 
   /**
diff --git a/web/modules/embed/src/Tests/EmbedButtonAdminTest.php b/web/modules/embed/src/Tests/EmbedButtonAdminTest.php
deleted file mode 100644
index 63c9c98b59..0000000000
--- a/web/modules/embed/src/Tests/EmbedButtonAdminTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-namespace Drupal\embed\Tests;
-
-/**
- * Tests the administrative UI.
- *
- * @group embed
- */
-class EmbedButtonAdminTest extends EmbedTestBase {
-
-  /**
-   * Tests the embed_button administration functionality.
-   */
-  public function testEmbedButtonAdmin() {
-    // Ensure proper access to the Embed settings page.
-    $this->drupalGet('admin/config/content/embed');
-    $this->assertResponse(403);
-
-    $this->drupalLogin($this->adminUser);
-    $this->drupalGet('admin/config/content/embed');
-    $this->assertResponse(200);
-
-    // Add embed button.
-    $this->clickLink('Add embed button');
-    $button_id = strtolower($this->randomMachineName());
-    $button_label = $this->randomMachineName();
-    $edit = [
-      'id' => $button_id,
-      'label' => $button_label,
-      'type_id' => 'embed_test_default',
-    ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
-    // Ensure that the newly created button is listed.
-    $this->drupalGet('admin/config/content/embed');
-    $this->assertText($button_label, 'Test embed_button appears on the list page');
-
-    // Edit embed button.
-    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id);
-    $new_button_label = $this->randomMachineName();
-    $edit = [
-      'label' => $new_button_label,
-    ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
-    // Ensure that name and label has been changed.
-    $this->drupalGet('admin/config/content/embed');
-    $this->assertText($new_button_label, 'New label appears on the list page');
-    $this->assertNoText($button_label, 'Old label does not appears on the list page');
-
-    // Delete embed button.
-    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id . '/delete');
-    $this->drupalPostForm(NULL, [], 'Delete');
-    // Ensure that the deleted embed button no longer exists.
-    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id);
-    $this->assertResponse(404, 'Deleted embed button no longer exists.');
-    // Ensure that the deleted button is no longer listed.
-    $this->drupalGet('admin/config/content/embed');
-    $this->assertNoText($button_label, 'Test embed_button does not appears on the list page');
-  }
-
-  public function testButtonValidation() {
-    $this->drupalLogin($this->adminUser);
-    $button_id = strtolower($this->randomMachineName());
-    $edit = [
-      'id' => $button_id,
-      'label' => $this->randomMachineName(),
-      'type_id' => 'embed_test_aircraft',
-    ];
-    $this->drupalPostAjaxForm('admin/config/content/embed/button/add', $edit, 'type_id');
-    $this->assertFieldByName('type_settings[aircraft_type]', 'fixed-wing');
-
-    $edit['type_settings[aircraft_type]'] = 'invalid';
-    $this->drupalPostForm(NULL, $edit, 'Save');
-    $this->assertText('Cannot select invalid aircraft type.');
-
-    $edit['type_settings[aircraft_type]'] = 'helicopters';
-    $this->drupalPostForm(NULL, $edit, 'Save');
-    $this->assertText('Helicopters are just rotorcraft.');
-
-    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id);
-    $this->assertFieldByName('type_settings[aircraft_type]', 'rotorcraft');
-  }
-
-}
diff --git a/web/modules/embed/src/Tests/EmbedTestBase.php b/web/modules/embed/src/Tests/EmbedTestBase.php
deleted file mode 100644
index 766748f84f..0000000000
--- a/web/modules/embed/src/Tests/EmbedTestBase.php
+++ /dev/null
@@ -1,224 +0,0 @@
-<?php
-
-namespace Drupal\embed\Tests;
-
-use Drupal\editor\Entity\Editor;
-use Drupal\file\Entity\File;
-use Drupal\filter\Entity\FilterFormat;
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Base class for all embed tests.
- */
-abstract class EmbedTestBase extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = [
-    'block',
-    'embed',
-    'embed_test',
-    'editor',
-    'ckeditor',
-  ];
-
-  /**
-   * The test administrative user.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $adminUser;
-
-  /**
-   * The test administrative user.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $webUser;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-
-    // Create Filtered HTML text format and enable entity_embed filter.
-    $format = FilterFormat::create([
-      'format' => 'embed_test',
-      'name' => 'Embed format',
-      'filters' => [],
-    ]);
-    $format->save();
-
-    $editor_group = [
-      'name' => 'Embed',
-      'items' => [
-        'embed_test_default',
-      ],
-    ];
-    $editor = Editor::create([
-      'format' => 'embed_test',
-      'editor' => 'ckeditor',
-      'settings' => [
-        'toolbar' => [
-          'rows' => [[$editor_group]],
-        ],
-      ],
-    ]);
-    $editor->save();
-
-    // Create a user with required permissions.
-    $this->adminUser = $this->drupalCreateUser([
-      'administer embed buttons',
-      'use text format embed_test',
-    ]);
-
-    // Create a user with required permissions.
-    $this->webUser = $this->drupalCreateUser([
-      'use text format embed_test',
-    ]);
-
-    // Set up some standard blocks for the testing theme (Classy).
-    // @see https://www.drupal.org/node/507488?page=1#comment-10291517
-    $this->drupalPlaceBlock('local_tasks_block');
-    $this->drupalPlaceBlock('local_actions_block');
-  }
-
-  /**
-   * Retrieves a sample file of the specified type.
-   *
-   * @return \Drupal\file\FileInterface
-   */
-  protected function getTestFile($type_name, $size = NULL) {
-    // Get a file to upload.
-    $file = current($this->drupalGetTestFiles($type_name, $size));
-
-    // Add a filesize property to files as would be read by
-    // \Drupal\file\Entity\File::load().
-    $file->filesize = filesize($file->uri);
-
-    $file = File::create((array) $file);
-    $file->save();
-    return $file;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * This is a duplicate of WebTestBase::drupalProcessAjaxResponse() that
-   * includes the fix from https://www.drupal.org/node/2554449 for using ID
-   * selectors in AJAX commands.
-   */
-  protected function drupalProcessAjaxResponse($content, array $ajax_response, array $ajax_settings, array $drupal_settings) {
-    // ajax.js applies some defaults to the settings object, so do the same
-    // for what's used by this function.
-    $ajax_settings += array(
-      'method' => 'replaceWith',
-    );
-    // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
-    // them.
-    $dom = new \DOMDocument();
-    @$dom->loadHTML($content);
-    // XPath allows for finding wrapper nodes better than DOM does.
-    $xpath = new \DOMXPath($dom);
-    foreach ($ajax_response as $command) {
-      // Error messages might be not commands.
-      if (!is_array($command)) {
-        continue;
-      }
-      switch ($command['command']) {
-        case 'settings':
-          $drupal_settings = NestedArray::mergeDeepArray([$drupal_settings, $command['settings']], TRUE);
-          break;
-
-        case 'insert':
-          $wrapperNode = NULL;
-          // When a command specifies a specific selector, use it.
-          if (!empty($command['selector']) && strpos($command['selector'], '#') === 0) {
-            $wrapperNode = $xpath->query('//*[@id="' . substr($command['selector'], 1) . '"]')->item(0);
-          }
-          // When a command doesn't specify a selector, use the
-          // #ajax['wrapper'] which is always an HTML ID.
-          elseif (!empty($ajax_settings['wrapper'])) {
-            $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0);
-          }
-          // @todo Ajax commands can target any jQuery selector, but these are
-          //   hard to fully emulate with XPath. For now, just handle 'head'
-          //   and 'body', since these are used by
-          //   \Drupal\Core\Ajax\AjaxResponse::ajaxRender().
-          elseif (in_array($command['selector'], array('head', 'body'))) {
-            $wrapperNode = $xpath->query('//' . $command['selector'])->item(0);
-          }
-          if ($wrapperNode) {
-            // ajax.js adds an enclosing DIV to work around a Safari bug.
-            $newDom = new \DOMDocument();
-            // DOM can load HTML soup. But, HTML soup can throw warnings,
-            // suppress them.
-            @$newDom->loadHTML('<div>' . $command['data'] . '</div>');
-            // Suppress warnings thrown when duplicate HTML IDs are encountered.
-            // This probably means we are replacing an element with the same ID.
-            $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE);
-            $method = isset($command['method']) ? $command['method'] : $ajax_settings['method'];
-            // The "method" is a jQuery DOM manipulation function. Emulate
-            // each one using PHP's DOMNode API.
-            switch ($method) {
-              case 'replaceWith':
-                $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode);
-                break;
-              case 'append':
-                $wrapperNode->appendChild($newNode);
-                break;
-              case 'prepend':
-                // If no firstChild, insertBefore() falls back to
-                // appendChild().
-                $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild);
-                break;
-              case 'before':
-                $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode);
-                break;
-              case 'after':
-                // If no nextSibling, insertBefore() falls back to
-                // appendChild().
-                $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling);
-                break;
-              case 'html':
-                foreach ($wrapperNode->childNodes as $childNode) {
-                  $wrapperNode->removeChild($childNode);
-                }
-                $wrapperNode->appendChild($newNode);
-                break;
-            }
-          }
-          break;
-
-        // @todo Add suitable implementations for these commands in order to
-        //   have full test coverage of what ajax.js can do.
-        case 'remove':
-          break;
-        case 'changed':
-          break;
-        case 'css':
-          break;
-        case 'data':
-          break;
-        case 'restripe':
-          break;
-        case 'add_css':
-          break;
-        case 'update_build_id':
-          $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0);
-          if ($buildId) {
-            $buildId->setAttribute('value', $command['new']);
-          }
-          break;
-      }
-    }
-    $content = $dom->saveHTML();
-    $this->setRawContent($content);
-    $this->setDrupalSettings($drupal_settings);
-  }
-
-}
diff --git a/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml b/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml
index 5e0f7b2b9f..a118f6b4d8 100644
--- a/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml
+++ b/web/modules/embed/tests/embed_test/config/install/embed.button.embed_test_default.yml
@@ -4,3 +4,4 @@ type_id: 'embed_test_default'
 dependencies:
   module:
     - embed_test
+icon: { }
diff --git a/web/modules/embed/tests/embed_test/embed_test.info.yml b/web/modules/embed/tests/embed_test/embed_test.info.yml
index 3f95b38bee..2aeb758413 100644
--- a/web/modules/embed/tests/embed_test/embed_test.info.yml
+++ b/web/modules/embed/tests/embed_test/embed_test.info.yml
@@ -1,14 +1,14 @@
 name: 'Embed test'
 type: module
 description: 'Support module for the Embed module tests.'
-# core: 8.x
+core: 8.x
+core_version_requirement: ^8 || ^9
 package: Testing
 dependencies:
   - embed
   - node
 
-# Information added by Drupal.org packaging script on 2017-03-29
-version: '8.x-1.0'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-05-22
+version: '8.x-1.4'
 project: 'embed'
-datestamp: 1490755690
+datestamp: 1590176834
diff --git a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php
index b344fa9bc0..72d2045da4 100644
--- a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php
+++ b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Aircraft.php
@@ -59,7 +59,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
     if ($form_state->getValue('aircraft_type') === 'helicopters') {
-      drupal_set_message($this->t('Helicopters are just rotorcraft.'), 'warning');
+      $this->messenger()->addWarning($this->t('Helicopters are just rotorcraft.'));
       $form_state->setValue('aircraft_type', 'rotorcraft');
     }
 
diff --git a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php
index 3221f8053a..c319f44683 100644
--- a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php
+++ b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/Animal.php
@@ -58,7 +58,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    * {@inheritdoc}
    */
   public function getDefaultIconUrl() {
-    return '';
+    return '/animal.png';
   }
 
 }
diff --git a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php
index 385bc5ff00..00f7bb8e2f 100644
--- a/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php
+++ b/web/modules/embed/tests/embed_test/src/Plugin/EmbedType/EmbedTestDefault.php
@@ -23,7 +23,7 @@ class EmbedTestDefault extends EmbedTypeBase {
    * {@inheritdoc}
    */
   public function getDefaultIconUrl() {
-    return '';
+    return '/default.png';
   }
 
 }
diff --git a/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php b/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php
index 719562de82..06589f95c0 100644
--- a/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php
+++ b/web/modules/embed/tests/embed_test/src/Plugin/Filter/EntityEmbedByID.php
@@ -2,9 +2,14 @@
 
 namespace Drupal\embed_test\Plugin\Filter;
 
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
-use Drupal\node\Entity\Node;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Renders a full node view from an embed code like node:NID.
@@ -16,7 +21,54 @@
  *   type = Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE,
  * )
  */
-class EntityEmbedByID extends FilterBase {
+class EntityEmbedByID extends FilterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a EntityEmbedByID object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('renderer')
+    );
+  }
 
   /**
    * {@inheritdoc}
@@ -28,9 +80,14 @@ public function process($text, $langcode) {
     preg_match_all('/node:([0-9]+)/', $text, $matches);
 
     foreach ($matches[0] as $i => $search) {
-      $node = Node::load($matches[1][$i]);
-      $build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node);
-      $replace = \Drupal::service('renderer')->render($build);
+      $replace = '';
+      if ($node = $this->entityTypeManager->getStorage('node')->load($matches[1][$i])) {
+        $build = $this->entityTypeManager->getViewBuilder('node')->view($node);
+        $replace = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$build) {
+          return $this->renderer->render($build);
+        });
+        $result = $result->merge(BubbleableMetadata::createFromRenderArray($build));
+      }
       $text = str_replace($search, $replace, $text);
     }
 
diff --git a/web/modules/embed/src/Tests/EmbedButtonEditorAccessCheckTest.php b/web/modules/embed/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php
similarity index 94%
rename from web/modules/embed/src/Tests/EmbedButtonEditorAccessCheckTest.php
rename to web/modules/embed/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php
index c73887acfc..e275664e03 100644
--- a/web/modules/embed/src/Tests/EmbedButtonEditorAccessCheckTest.php
+++ b/web/modules/embed/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php
@@ -1,8 +1,9 @@
 <?php
 
-namespace Drupal\embed\Tests;
+namespace Drupal\Tests\embed\Functional;
 
 use Drupal\editor\Entity\Editor;
+use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 
 /**
  * Tests EmbedButtonEditorAccessCheck.
@@ -11,8 +12,15 @@
  */
 class EmbedButtonEditorAccessCheckTest extends EmbedTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
   const SUCCESS = 'Success!';
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * Tests \Drupal\embed\Access\EmbedButtonEditorAccessCheck.
    */
diff --git a/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php b/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php
index fbbd8f367e..bf2e581b93 100644
--- a/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php
+++ b/web/modules/embed/tests/src/Functional/EmbedPreviewTest.php
@@ -20,6 +20,11 @@ class EmbedPreviewTest extends BrowserTestBase {
    */
   public static $modules = ['embed_test', 'filter'];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * Tests that out-of-band assets are included with previews.
    */
@@ -53,6 +58,7 @@ public function testPreview() {
     $response = $this->drupalGet('/embed/preview/foo', [
       'query' => [
         'value' => 'node:' . $node->id(),
+        '_wrapper_format' => 'drupal_ajax',
       ],
     ]);
 
diff --git a/web/modules/embed/tests/src/Functional/EmbedTestBase.php b/web/modules/embed/tests/src/Functional/EmbedTestBase.php
new file mode 100644
index 0000000000..17f990a3c4
--- /dev/null
+++ b/web/modules/embed/tests/src/Functional/EmbedTestBase.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\Tests\embed\Functional;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\file\Entity\File;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\TestFileCreationTrait;
+
+/**
+ * Base class for all embed tests.
+ */
+abstract class EmbedTestBase extends BrowserTestBase {
+
+  use TestFileCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'block',
+    'embed',
+    'embed_test',
+    'editor',
+    'ckeditor',
+  ];
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $webUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create Filtered HTML text format and enable entity_embed filter.
+    $format = FilterFormat::create([
+      'format' => 'embed_test',
+      'name' => 'Embed format',
+      'filters' => [],
+    ]);
+    $format->save();
+
+    $editor_group = [
+      'name' => 'Embed',
+      'items' => [
+        'embed_test_default',
+      ],
+    ];
+    $editor = Editor::create([
+      'format' => 'embed_test',
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [[$editor_group]],
+        ],
+      ],
+    ]);
+    $editor->save();
+
+    // Create a user with required permissions.
+    $this->adminUser = $this->drupalCreateUser([
+      'administer embed buttons',
+      'use text format embed_test',
+    ]);
+
+    // Create a user with required permissions.
+    $this->webUser = $this->drupalCreateUser([
+      'use text format embed_test',
+    ]);
+
+    // Set up some standard blocks for the testing theme (Classy).
+    // @see https://www.drupal.org/node/507488?page=1#comment-10291517
+    $this->drupalPlaceBlock('local_tasks_block');
+    $this->drupalPlaceBlock('local_actions_block');
+  }
+
+  /**
+   * Retrieves a sample file of the specified type.
+   *
+   * @return \Drupal\file\FileInterface
+   */
+  protected function getTestFile($type_name, $size = NULL) {
+    // Get a file to upload.
+    $file = current($this->getTestFiles($type_name, $size));
+
+    // Add a filesize property to files as would be read by
+    // \Drupal\file\Entity\File::load().
+    $file->filesize = filesize($file->uri);
+
+    $file = File::create((array) $file);
+    $file->save();
+    return $file;
+  }
+
+}
diff --git a/web/modules/embed/src/Tests/PreviewTest.php b/web/modules/embed/tests/src/Functional/PreviewTest.php
similarity index 90%
rename from web/modules/embed/src/Tests/PreviewTest.php
rename to web/modules/embed/tests/src/Functional/PreviewTest.php
index 11613ee3e5..1163240327 100644
--- a/web/modules/embed/src/Tests/PreviewTest.php
+++ b/web/modules/embed/tests/src/Functional/PreviewTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\embed\Tests;
+namespace Drupal\Tests\embed\Functional;
 
 /**
  * Tests the preview controller and route.
@@ -11,6 +11,11 @@ class PreviewTest extends EmbedTestBase {
 
   const SUCCESS = 'Success!';
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * Tests the route used for generating preview of embedding entities.
    */
@@ -60,7 +65,7 @@ public function getRoute($filter_format_id, $value = NULL) {
     if (!isset($value)) {
       $value = static::SUCCESS;
     }
-    return $this->drupalGet($url, ['query' => ['value' => $value]]);
+    return $this->drupalGet($url, ['query' => ['text' => $value]]);
   }
 
 }
diff --git a/web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php b/web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php
new file mode 100644
index 0000000000..767cf43e31
--- /dev/null
+++ b/web/modules/embed/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Drupal\Tests\embed\FunctionalJavascript;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the administrative UI.
+ *
+ * @group embed
+ */
+class EmbedButtonAdminTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'block',
+    'embed',
+    'embed_test',
+    'editor',
+    'ckeditor',
+  ];
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $webUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create Filtered HTML text format and enable entity_embed filter.
+    $format = FilterFormat::create([
+      'format' => 'embed_test',
+      'name' => 'Embed format',
+      'filters' => [],
+    ]);
+    $format->save();
+
+    $editor_group = [
+      'name' => 'Embed',
+      'items' => [
+        'embed_test_default',
+      ],
+    ];
+    $editor = Editor::create([
+      'format' => 'embed_test',
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [[$editor_group]],
+        ],
+      ],
+    ]);
+    $editor->save();
+
+    // Create a user with required permissions.
+    $this->adminUser = $this->drupalCreateUser([
+      'administer embed buttons',
+      'use text format embed_test',
+    ]);
+
+    // Create a user with required permissions.
+    $this->webUser = $this->drupalCreateUser([
+      'use text format embed_test',
+    ]);
+
+    // Set up some standard blocks for the testing theme (Classy).
+    // @see https://www.drupal.org/node/507488?page=1#comment-10291517
+    $this->drupalPlaceBlock('local_tasks_block');
+    $this->drupalPlaceBlock('local_actions_block');
+  }
+
+  /**
+   * Tests the embed_button administration functionality.
+   */
+  public function testEmbedButtonAdmin() {
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    // Ensure proper access to the Embed settings page.
+    $this->drupalGet('admin/config/content/embed');
+    $assert_session->pageTextContains('You are not authorized to access this page.');
+
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('admin/config/content/embed');
+
+    // Add embed button.
+    $this->clickLink('Add embed button');
+    $button_label = $this->randomMachineName();
+    $button_id = strtolower($button_label);
+    $page->fillField('label', $button_label);
+    $this->assertNotEmpty($assert_session->waitForText("Machine name: $button_id"));
+    $edit = [
+      'type_id' => 'embed_test_default',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    // Ensure that the newly created button is listed.
+    $this->drupalGet('admin/config/content/embed');
+    $assert_session->pageTextContains($button_label);
+
+    // Edit embed button.
+    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id);
+    $new_button_label = $this->randomMachineName();
+    $edit = [
+      'label' => $new_button_label,
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    // Ensure that name and label has been changed.
+    $this->drupalGet('admin/config/content/embed');
+    $assert_session->pageTextContains($new_button_label);
+    $assert_session->pageTextNotContains($button_label);
+
+    // Delete embed button.
+    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id . '/delete');
+    $this->drupalPostForm(NULL, [], 'Delete');
+    // Ensure that the deleted embed button no longer exists.
+    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id);
+    $assert_session->pageTextContains('The requested page could not be found.');
+    // Ensure that the deleted button is no longer listed.
+    $this->drupalGet('admin/config/content/embed');
+    $assert_session->pageTextNotContains($button_label);
+  }
+
+  public function testButtonValidation() {
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('admin/config/content/embed/button/add');
+
+    $button_label = $this->randomMachineName();
+    $button_id = strtolower($button_label);
+    $page->fillField('label', $button_label);
+    $this->assertNotEmpty($assert_session->waitForText("Machine name: $button_id"));
+    $page->selectFieldOption('type_id', 'embed_test_aircraft');
+    $aircraft_type = $assert_session->waitForField('type_settings[aircraft_type]');
+    $this->assertNotEmpty($aircraft_type);
+    $this->assertSame('fixed-wing', $aircraft_type->getValue());
+
+    $edit['type_settings[aircraft_type]'] = 'invalid';
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    $assert_session->pageTextContains('Cannot select invalid aircraft type.');
+
+    $edit['type_settings[aircraft_type]'] = 'helicopters';
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    $assert_session->pageTextContains('Helicopters are just rotorcraft.');
+
+    $this->drupalGet('admin/config/content/embed/button/manage/' . $button_id);
+    $this->assertFieldByName('type_settings[aircraft_type]', 'rotorcraft');
+  }
+
+  public function testCKEditorButtonConflict() {
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('admin/config/content/embed/button/add');
+
+    $button_label = $this->randomMachineName();
+    $button_id = strtolower($button_label);
+    $page->fillField('label', $button_label);
+    $this->assertNotEmpty($assert_session->waitForText("Machine name: $button_id"));
+
+    $assert_session->elementExists('css', '#edit-label-machine-name-suffix')
+      ->pressButton('Edit');
+
+    $id = $assert_session->waitForField('id');
+    $this->assertNotEmpty($id);
+    $id->setValue('DrupalImage');
+
+    $edit = [
+      'type_id' => 'embed_test_default',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Save');
+  }
+
+}
diff --git a/web/modules/embed/tests/src/Kernel/IconFileUsageTest.php b/web/modules/embed/tests/src/Kernel/IconFileUsageTest.php
deleted file mode 100644
index d815426f8c..0000000000
--- a/web/modules/embed/tests/src/Kernel/IconFileUsageTest.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-namespace Drupal\Tests\embed\Kernel;
-
-use Drupal\embed\Entity\EmbedButton;
-use Drupal\file\Entity\File;
-use Drupal\KernelTests\KernelTestBase;
-
-/**
- * Tests embed button icon file usage.
- *
- * @group embed
- */
-class IconFileUsageTest extends KernelTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['embed', 'embed_test'];
-
-  /**
-   * Tests the embed_button and file usage integration.
-   */
-  public function testEmbedButtonIconUsage() {
-    $this->enableModules(['system', 'user', 'file']);
-
-    $this->installSchema('file', ['file_usage']);
-    $this->installConfig(['system']);
-    $this->installEntitySchema('user');
-    $this->installEntitySchema('file');
-    $this->installEntitySchema('embed_button');
-
-    $file1 = file_save_data(file_get_contents('core/misc/druplicon.png'));
-    $file1->setTemporary();
-    $file1->save();
-
-    $file2 = file_save_data(file_get_contents('core/misc/druplicon.png'));
-    $file2->setTemporary();
-    $file2->save();
-
-    $button = [
-      'id' => 'test_button',
-      'label' => 'Testing embed button instance',
-      'type_id' => 'embed_test_default',
-      'icon_uuid' => $file1->uuid(),
-    ];
-
-    $entity = EmbedButton::create($button);
-    $entity->save();
-    $this->assertTrue(File::load($file1->id())->isPermanent());
-
-    // Delete the icon from the button.
-    $entity->icon_uuid = NULL;
-    $entity->save();
-    $this->assertTrue(File::load($file1->id())->isTemporary());
-
-    $entity->icon_uuid = $file1->uuid();
-    $entity->save();
-    $this->assertTrue(File::load($file1->id())->isPermanent());
-
-    $entity->icon_uuid = $file2->uuid();
-    $entity->save();
-    $this->assertTrue(File::load($file1->id())->isTemporary());
-    $this->assertTrue(File::load($file2->id())->isPermanent());
-
-    $entity->delete();
-    $this->assertTrue(File::load($file2->id())->isTemporary());
-  }
-
-}
diff --git a/web/modules/embed/tests/src/Kernel/IconTest.php b/web/modules/embed/tests/src/Kernel/IconTest.php
new file mode 100644
index 0000000000..ca34d46d30
--- /dev/null
+++ b/web/modules/embed/tests/src/Kernel/IconTest.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\Tests\embed\Kernel;
+
+use Drupal\embed\EmbedButtonInterface;
+use Drupal\embed\Entity\EmbedButton;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests embed button icon file handling.
+ *
+ * @group embed
+ * @coversDefaultClass \Drupal\embed\Entity\EmbedButton
+ */
+class IconTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'system',
+    'embed',
+    'embed_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installConfig('system');
+    $this->installEntitySchema('embed_button');
+  }
+
+  /**
+   * Tests the icon functionality.
+   *
+   * @covers ::convertImageToEncodedData
+   * @covers ::convertEncodedDataToImage
+   * @covers ::getIconUrl
+   */
+  public function testIcon() {
+    $button = EmbedButton::create([
+      'id' => 'test',
+      'label' => 'Test',
+      'type_id' => 'embed_test_default',
+    ]);
+    $this->assertEmpty($button->icon);
+    $this->assertIconUrl('/default.png', $button);
+
+    $uri = 'public://button.png';
+    $image_contents = file_get_contents('core/misc/favicon.ico');
+    file_put_contents($uri, $image_contents);
+
+    $button->set('icon', EmbedButton::convertImageToEncodedData($uri));
+    $this->assertSame([
+      'data' => base64_encode($image_contents),
+      'uri' => $uri,
+    ], $button->icon);
+    $this->assertIconUrl($uri, $button);
+
+    // Delete the file and call getIconUrl and test that it recreated the file.
+    unlink($uri);
+    $this->assertFalse(is_file($uri));
+    $this->assertIconUrl($uri, $button);
+    $this->assertTrue(is_file($uri));
+    $this->assertSame(file_get_contents($uri), file_get_contents('core/misc/favicon.ico'));
+
+    // Test a manual, external URL for the icon image.
+    $button->set('icon', [
+      'uri' => 'http://www.example.com/button.png',
+    ]);
+    $this->assertIconUrl('http://www.example.com/button.png', $button);
+  }
+
+  /**
+   * Test a button's icon URL.
+   *
+   * @param string $uri
+   *   The exepcted URI to the icon file.
+   * @param \Drupal\embed\EmbedButtonInterface $button
+   *   The embed button.
+   * @param string $message
+   *   The assertion message.
+   */
+  protected function assertIconUrl($uri, EmbedButtonInterface $button, string $message = '') {
+    $this->assertSame(file_url_transform_relative(file_create_url($uri)), $button->getIconUrl(), $message);
+  }
+
+}
diff --git a/web/modules/entity_embed/.travis-before-script.sh b/web/modules/entity_embed/.travis-before-script.sh
deleted file mode 100644
index 8c29b41ef7..0000000000
--- a/web/modules/entity_embed/.travis-before-script.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-set -e $DRUPAL_TI_DEBUG
-
-# Ensure the right Drupal version is installed.
-# Note: This function is re-entrant.
-drupal_ti_ensure_drupal
-
-# Download dependencies
-mkdir -p "$DRUPAL_TI_DRUPAL_DIR/$DRUPAL_TI_MODULES_PATH"
-cd "$DRUPAL_TI_DRUPAL_DIR/$DRUPAL_TI_MODULES_PATH"
-git clone --depth 1 --branch 8.x-1.x https://github.com/drupal-media/embed.git
diff --git a/web/modules/entity_embed/.travis.yml b/web/modules/entity_embed/.travis.yml
deleted file mode 100644
index 7db1ba782b..0000000000
--- a/web/modules/entity_embed/.travis.yml
+++ /dev/null
@@ -1,117 +0,0 @@
-# @file
-# .travis.yml - Drupal for Travis CI Integration
-#
-# Template provided by https://github.com/LionsAd/drupal_ti.
-#
-# Based for simpletest upon:
-#   https://github.com/sonnym/travis-ci-drupal-module-example
-
-language: php
-sudo: false
-    
-cache:
-  directories:
-    - $HOME/.composer/cache
-
-php:
-  - 5.5
-  - 5.6
-  - 7
-  - hhvm
-
-matrix:
-  fast_finish: true
-  allow_failures:
-    - php: 7
-    - php: hhvm
-
-branches:
-  only:
-    - "8.x-1.x"
-
-env:
-  global:
-    # add composer's global bin directory to the path
-    # see: https://github.com/drush-ops/drush#install---composer
-    - PATH="$PATH:$HOME/.composer/vendor/bin"
-
-    # Configuration variables.
-    - DRUPAL_TI_MODULE_NAME="entity_embed"
-    - DRUPAL_TI_SIMPLETEST_GROUP="entity_embed"
-
-    # Define runners and environment vars to include before and after the
-    # main runners / environment vars.
-    #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./drupal_ti/before"
-    #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after"
-
-    # The environment to use, supported are: drupal-7, drupal-8
-    - DRUPAL_TI_ENVIRONMENT="drupal-8"
-
-    # Drupal specific variables.
-    - DRUPAL_TI_DB="drupal_travis_db"
-    - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db"
-    # Note: Do not add a trailing slash here.
-    - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1"
-    - DRUPAL_TI_WEBSERVER_PORT="8080"
-
-    # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end.
-    - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT"
-
-    # === Behat specific variables.
-    # This is relative to $TRAVIS_BUILD_DIR
-    - DRUPAL_TI_BEHAT_DIR="./tests/behat"
-    # These arguments are passed to the bin/behat command.
-    - DRUPAL_TI_BEHAT_ARGS=""
-    # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables.
-    - DRUPAL_TI_BEHAT_YML="behat.yml.dist"
-    # This is used to setup Xvfb.
-    - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16"
-    # The version of seleniumthat should be used.
-    - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44"
-    # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here.
-    - DRUPAL_TI_BEHAT_DRIVER="phantomjs"
-    - DRUPAL_TI_BEHAT_BROWSER="firefox"
-
-    # PHPUnit specific commandline arguments.
-    - DRUPAL_TI_PHPUNIT_ARGS=""
-
-    # Code coverage via coveralls.io
-    - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*"
-    # This needs to match your .coveralls.yml file.
-    - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml"
-
-    # Debug options
-    #- DRUPAL_TI_DEBUG="-x -v"
-    # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium",
-    # etc. to only output those channels.
-    #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver"
-
-  matrix:
-    # [[[ SELECT ANY OR MORE OPTIONS ]]]
-    #- DRUPAL_TI_RUNNERS="phpunit"
-    - DRUPAL_TI_RUNNERS="simpletest"
-    #- DRUPAL_TI_RUNNERS="behat"
-    #- DRUPAL_TI_RUNNERS="phpunit simpletest behat"
-
-mysql:
-  database: drupal_travis_db
-  username: root
-  encoding: utf8
-
-before_install:
-  - composer self-update
-  - composer global require "lionsad/drupal_ti:1.*"
-  - drupal-ti before_install
-
-install:
-  - drupal-ti install
-
-before_script:
-  - drupal-ti --include .travis-before-script.sh
-  - drupal-ti before_script
-
-script:
-  - drupal-ti script
-
-after_script:
-  - drupal-ti after_script
diff --git a/web/modules/entity_embed/DEVELOPING.md b/web/modules/entity_embed/DEVELOPING.md
deleted file mode 100644
index 15a659f19a..0000000000
--- a/web/modules/entity_embed/DEVELOPING.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Developing
-
-* Issues should be filed at http://drupal.org/project/issues/entity_embed
-* Pull requests can be made against https://github.com/drupal-media/entity_embed/pulls
diff --git a/web/modules/entity_embed/README.md b/web/modules/entity_embed/README.md
index a13275ecf7..a11575c2f4 100644
--- a/web/modules/entity_embed/README.md
+++ b/web/modules/entity_embed/README.md
@@ -1,8 +1,5 @@
 # Entity Embed Module
 
-[![Travis build status](https://img.shields.io/travis/drupal-media/entity_embed/8.x-1.x.svg)](https://travis-ci.org/drupal-media/entity_embed)
-[![Scrutinizer code quality](https://img.shields.io/scrutinizer/g/drupal-media/entity_embed/8.x-1.x.svg)](https://scrutinizer-ci.com/g/drupal-media/entity_embed)
-
 [Entity Embed](https://www.drupal.org/project/entity_embed) module
 allows any entity to be embedded using a text editor.
 
@@ -30,7 +27,7 @@ Entity Embed can be installed via the
   * If the text format uses the 'Limit allowed HTML tags and correct
     faulty HTML' filter, ensure the necessary tags and attributes were
     automatically whitelisted:
-    ```<drupal-entity data-entity-type data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button>```
+    ```<drupal-entity data-entity-type data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title>```
     appears in the 'Allowed HTML tags' setting.
     *Warning: If you were using the module in very early pre-alpha
     stages you might need to add `data-entity-id` to the list of allowed
@@ -90,7 +87,10 @@ different Entity Embed Display plugins out of the box:
   formatter. This will only work if the entity is a file entity type.
 - image:_formatter_id_: Renders the entity using a specific Image field
   formatter. This will only work if the entity is a file entity type,
-  and the file is an image.
+  and the file is an image.  For the alt and title text to save, the `alt`
+  and `title` attributes must be allowed on the `<drupal-entity>` HTML tag
+  in the "Allowed HTML tags" for text formats that have the "Limit allowed
+  HTML tags and correct faulty HTML" filter enabled.
 
 Configuration for the Entity Embed Display plugin can be provided by
 using a `data-entity-embed-display-settings` attribute, which contains a
diff --git a/web/modules/entity_embed/composer.json b/web/modules/entity_embed/composer.json
index c10ecf4038..5fd24c40b6 100644
--- a/web/modules/entity_embed/composer.json
+++ b/web/modules/entity_embed/composer.json
@@ -7,5 +7,12 @@
     "issues": "https://www.drupal.org/project/issues/entity_embed",
     "irc": "irc://irc.freenode.org/drupal-media"
   },
-  "license": "GPL-2.0+"
+  "license": "GPL-2.0+",
+  "require": {
+    "drupal/core": "^8.8 || ^9",
+    "drupal/embed": "^1.3"
+  },
+  "require-dev": {
+    "drupal/entity_browser": "^2.2"
+  }
 }
diff --git a/web/modules/entity_embed/css/entity_embed.dialog.css b/web/modules/entity_embed/css/entity_embed.dialog.css
index 5dabd951cb..23d7f0ec3f 100644
--- a/web/modules/entity_embed/css/entity_embed.dialog.css
+++ b/web/modules/entity_embed/css/entity_embed.dialog.css
@@ -13,18 +13,18 @@
 
 @media screen and (min-width: 768px) {
   .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select {
-    min-width: 730px; // 95%
+    min-width: 730px; /* 95% */
   }
 }
 
 @media screen and (min-width: 1000px) {
   .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select {
-    min-width: 950px; // 95%
+    min-width: 950px; /* 95% */
   }
 }
 
 @media screen and (min-width: 1200px) {
   .ui-dialog--narrow.entity-select-dialog .entity-embed-dialog-step--select {
-    min-width: 1140px; // 95%
+    min-width: 1140px; /* 95% */
   }
 }
diff --git a/web/modules/entity_embed/css/entity_embed.editor.css b/web/modules/entity_embed/css/entity_embed.editor.css
new file mode 100644
index 0000000000..f453a09150
--- /dev/null
+++ b/web/modules/entity_embed/css/entity_embed.editor.css
@@ -0,0 +1,15 @@
+/**
+ * @file
+ * Styles for CKEditor iframe.
+ */
+
+drupal-entity {
+  display: inline-block;
+}
+drupal-entity[data-align=left],
+drupal-entity[data-align=right] {
+  display: inline;
+}
+drupal-entity[data-align=center] {
+  display: flex;
+}
diff --git a/web/modules/entity_embed/css/entity_embed.filter.caption.css b/web/modules/entity_embed/css/entity_embed.filter.caption.css
new file mode 100644
index 0000000000..d1648f4ed9
--- /dev/null
+++ b/web/modules/entity_embed/css/entity_embed.filter.caption.css
@@ -0,0 +1,10 @@
+/**
+ * @file
+ * Caption filter: default styling for displaying Entity Embed captions.
+ */
+
+.caption .media .field,
+.caption .media .field * {
+  float: none;
+  margin: unset;
+}
diff --git a/web/modules/entity_embed/entity_embed.api.php b/web/modules/entity_embed/entity_embed.api.php
index fa762ad125..ec83c6e91e 100644
--- a/web/modules/entity_embed/entity_embed.api.php
+++ b/web/modules/entity_embed/entity_embed.api.php
@@ -72,7 +72,7 @@ function hook_entity_embed_context_alter(array &$context, \Drupal\Core\Entity\En
 }
 
 /**
- * Alter the context of an particular embedded entity type before it is rendered.
+ * Alter the context of a particular embedded entity type before it is rendered.
  *
  * @param array &$context
  *   The context array.
diff --git a/web/modules/entity_embed/entity_embed.info.yml b/web/modules/entity_embed/entity_embed.info.yml
index b0927604f3..23026e80f1 100644
--- a/web/modules/entity_embed/entity_embed.info.yml
+++ b/web/modules/entity_embed/entity_embed.info.yml
@@ -1,17 +1,17 @@
 name: Entity Embed
 type: module
 description: 'Allows entities to be embedded using a text editor.'
-# core: 8.x
+core_version_requirement: ^8.8 || ^9
 package: Filters
 dependencies:
   - drupal:editor
   - embed:embed
   - drupal:filter
+  - drupal:system
 test_dependencies:
   - entity_browser:entity_browser
 
-# Information added by Drupal.org packaging script on 2016-10-17
-version: '8.x-1.0-beta2'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-03-26
+version: '8.x-1.1'
 project: 'entity_embed'
-datestamp: 1476698379
+datestamp: 1585252809
diff --git a/web/modules/entity_embed/entity_embed.install b/web/modules/entity_embed/entity_embed.install
index 30a70f6641..6ebd6fa7fc 100644
--- a/web/modules/entity_embed/entity_embed.install
+++ b/web/modules/entity_embed/entity_embed.install
@@ -5,6 +5,8 @@
  * Contains install and update functions for Entity Embed.
  */
 
+use Drupal\Core\Entity\ContentEntityType;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\embed\Entity\EmbedButton;
 use Drupal\Core\Utility\UpdateException;
 
@@ -55,8 +57,7 @@ function entity_embed_update_8002() {
 }
 
 /**
- * Updates allowed HTML for all filter format config entities that have an
- * Entity Embed button.
+ * Updates allowed HTML for all filter configs that have an Entity Embed button.
  */
 function entity_embed_update_8003() {
   $buttons = \Drupal::entityTypeManager()->getStorage('embed_button')->loadMultiple();
@@ -85,3 +86,16 @@ function entity_embed_update_8003() {
     }
   }
 }
+
+/**
+ * Adds new content entity type to remove dependency on node module.
+ */
+function entity_embed_update_8004() {
+  \Drupal::entityDefinitionUpdateManager()->installEntityType(new ContentEntityType([
+    'id' => 'entity_embed_fake_entity',
+    'label' => new TranslatableMarkup('Fake entity type'),
+    'handlers' => [
+      'storage' => 'Drupal\\Core\\Entity\\ContentEntityNullStorage',
+    ],
+  ]));
+}
diff --git a/web/modules/entity_embed/entity_embed.libraries.yml b/web/modules/entity_embed/entity_embed.libraries.yml
index 5eca8e772c..d56978dc70 100644
--- a/web/modules/entity_embed/entity_embed.libraries.yml
+++ b/web/modules/entity_embed/entity_embed.libraries.yml
@@ -8,3 +8,11 @@ drupal.entity_embed.dialog:
   dependencies:
     - core/drupal
     - core/jquery
+
+caption:
+  version: VERSION
+  css:
+    component:
+      css/entity_embed.filter.caption.css: {}
+  dependencies:
+    - filter/caption
diff --git a/web/modules/entity_embed/entity_embed.module b/web/modules/entity_embed/entity_embed.module
index ee8fbaee84..a531922b78 100644
--- a/web/modules/entity_embed/entity_embed.module
+++ b/web/modules/entity_embed/entity_embed.module
@@ -2,12 +2,33 @@
 
 /**
  * @file
- * Framework for allowing entities to be embedded using CKEditor plugin and text
- * format.
+ * Framework for allowing entities to be embedded in CKEditor.
  */
 
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Implements hook_help().
+ */
+function entity_embed_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.entity_embed':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Entity Embed module allows entities to be embedded in formatted text.') . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<dl>';
+      $output .= '<dt>' . t('Embedding media') . '</dt>';
+      $output .= '<dd>' . t('This module, and the text filter that it provides along with the CKEditor integration, is especially suited to allow content authors to embed media in their textual content: images, video, and so on.') . '</dd>';
+      $output .= '<dt>' . t('Embedding arbitrary content') . '</dt>';
+      $output .= '<dd>' . t('As mentioned above, this module is especially helpful for embedding media in textual content, but it is not necessarily restricted to that; it allows <em>any</em> entity to be embedded. On an e-commerce site, you may want to embed products, on a company blog you may want to embed past projects, and so on.') . '</dd>';
+      $output .= '</dl>';
+      return $output;
+  }
+}
 
 /**
  * Implements hook_theme().
@@ -30,33 +51,16 @@ function entity_embed_theme() {
  *   - element: An associative array containing the properties of the element.
  *     Properties used: #attributes, #children.
  */
-function template_preprocess_entity_embed_container(&$variables) {
+function template_preprocess_entity_embed_container(array &$variables) {
   $variables['element'] += ['#attributes' => []];
   $variables['attributes'] = $variables['element']['#attributes'];
   $variables['children'] = $variables['element']['#children'];
-    if (!empty($variables['element']['#context']['data-entity-embed-display-settings']['link_url'])) {
-    $link = UrlHelper::filterBadProtocol($variables['element']['#context']['data-entity-embed-display-settings']['link_url']);
-    if (!UrlHelper::isExternal($link)) {
-      $link = 'internal:/' . ltrim($link, '/');
-    }
-    $link = Url::fromUri($link);
-    $attributes = [];
-    if (!empty($variables['element']['#context']['data-entity-embed-display-settings']['link_url_target']) && $variables['element']['#context']['data-entity-embed-display-settings']['link_url_target'] == 1) {
-      $attributes = ['attributes' => ['target' => '_blank']];
-    }
-    $variables['children'] = [
-      [
-        '#type' => 'link',
-        '#title' => $variables['children'],
-        '#options' => $attributes,
-        '#url' => $link,
-      ]
-    ];
-  }
 }
 
 /**
- * Implements hook_entity_embed_display_plugins_alter() on behalf of file.module.
+ * Implements hook_entity_embed_display_plugins_alter().
+ *
+ * Implementation on behalf of the file module.
  */
 function file_entity_embed_display_plugins_alter(array &$plugins) {
   // The RSS enclosure field formatter is not usable for Entity Embed.
@@ -64,7 +68,9 @@ function file_entity_embed_display_plugins_alter(array &$plugins) {
 }
 
 /**
- * Implements hook_entity_embed_display_plugins_alter() on behalf of taxonomy.module.
+ * Implements hook_entity_embed_display_plugins_alter().
+ *
+ * Implementation on behalf of the taxonomy module.
  */
 function taxonomy_entity_embed_display_plugins_alter(array &$plugins) {
   // The RSS category field formatter is not usable for Entity Embed.
@@ -86,3 +92,195 @@ function entity_embed_entity_embed_display_plugins_for_context_alter(array &$def
     unset($definitions['entity_reference:entity_reference_entity_view']);
   }
 }
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function entity_embed_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+  // Add an additional validate callback so we can ensure the order of filters
+  // is correct.
+  $form['#validate'][] = 'entity_embed_filter_format_edit_form_validate';
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function entity_embed_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+  // Add an additional validate callback so we can ensure the order of filters
+  // is correct.
+  $form['#validate'][] = 'entity_embed_filter_format_edit_form_validate';
+}
+
+/**
+ * Validate callback to ensure filter order and allowed_html are compatible.
+ */
+function entity_embed_filter_format_edit_form_validate($form, FormStateInterface $form_state) {
+  // This validate handler is not applicable when using the 'Configure' button.
+  if ($form_state->getTriggeringElement()['#name'] === 'editor_configure') {
+    return;
+  }
+
+  $allowed_html_path = [
+    'filters',
+    'filter_html',
+    'settings',
+    'allowed_html',
+  ];
+
+  $button_group_path = [
+    'editor',
+    'settings',
+    'toolbar',
+    'button_groups',
+  ];
+
+  $filter_html_settings_path = [
+    'filters',
+    'filter_html',
+    'settings',
+  ];
+
+  $filter_html_enabled = $form_state->getValue([
+    'filters',
+    'filter_html',
+    'status',
+  ]);
+
+  $entity_embed_enabled = $form_state->getValue([
+    'filters',
+    'entity_embed',
+    'status',
+  ]);
+
+  if ($entity_embed_enabled && $filter_html_enabled && $allowed_html = $form_state->getValue($allowed_html_path)) {
+    if ($button_groups = $form_state->getValue($button_group_path)) {
+      $buttons = [];
+      $button_groups = json_decode($button_groups, TRUE);
+      if (!empty($button_groups[0])) {
+        foreach ($button_groups[0] as $button_group) {
+          foreach ($button_group['items'] as $item) {
+            $buttons[] = $item;
+          }
+        }
+      }
+
+      /** @var \Drupal\filter\Entity\FilterFormat $filter_format */
+      $filter_format = $form_state->getFormObject()->getEntity();
+
+      $filter_html = $filter_format->filters()->get('filter_html');
+      $filter_html->setConfiguration(['settings' => $form_state->getValue($filter_html_settings_path)]);
+      $restrictions = $filter_html->getHTMLRestrictions();
+      $allowed = $restrictions['allowed'];
+
+      $embeds = \Drupal::entityTypeManager()
+        ->getStorage('embed_button')
+        ->loadMultiple($buttons);
+
+      /** @var \Drupal\embed\Entity\EmbedButton $embed */
+      foreach ($embeds as $embed) {
+        if ($embed->getTypeId() !== 'entity') {
+          continue;
+        }
+        // Require `<drupal-entity>` HTML tag if filter_html is enabled.
+        if (!isset($allowed['drupal-entity'])) {
+          $form_state->setError($form['filters']['settings']['filter_html']['allowed_html'], t('The %embed button requires <code>&lt;drupal-entity&gt;</code> among the allowed HTML tags.', [
+            '%embed' => $embed->label(),
+          ]));
+          break;
+        }
+        else {
+          $required_attributes = [
+            'data-entity-type',
+            'data-entity-uuid',
+            'data-entity-embed-display',
+            'data-entity-embed-display-settings',
+            'data-align',
+            'data-caption',
+            'data-embed-button',
+            'alt',
+            'title',
+          ];
+
+          // If there are no attributes, the allowed item is set to FALSE,
+          // otherwise, it is set to an array.
+          if ($allowed['drupal-entity'] === FALSE) {
+            $missing_attributes = $required_attributes;
+          }
+          else {
+            $missing_attributes = array_diff($required_attributes, array_keys($allowed['drupal-entity']));
+          }
+
+          if ($missing_attributes) {
+            $form_state->setError($form['filters']['settings']['filter_html']['allowed_html'], t('The <code>&lt;drupal-entity&gt;</code> tag in the allowed HTML tags is missing the following attributes: <code>%list</code>.', [
+              '%list' => implode(', ', $missing_attributes),
+            ]));
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  $filters = $form_state->getValue('filters');
+
+  $get_filter_label = function ($filter_plugin_id) use ($form) {
+    return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup'];
+  };
+
+  // The "entity_embed" filter must run after "filter_align", "filter_caption",
+  // and "filter_html_image_secure".
+  if ($entity_embed_enabled) {
+    $precedents = [
+      'filter_align',
+      'filter_caption',
+      'filter_html_image_secure',
+    ];
+
+    $error_filters = [];
+    foreach ($precedents as $filter_name) {
+
+      // A filter that should run before entity embed filter.
+      $precedent = $filters[$filter_name];
+
+      if (empty($precedent['status']) || !isset($precedent['weight'])) {
+        continue;
+      }
+
+      if ($precedent['weight'] >= $filters['entity_embed']['weight']) {
+        $error_filters[$filter_name] = $get_filter_label($filter_name);
+      }
+    }
+
+    if (!empty($error_filters)) {
+      $singular = 'The %entity-embed-filter-label filter needs to be placed after the %filter filter.';
+      $plural = 'The %entity-embed-filter-label filter needs to be placed after the following filters: %filters.';
+      $error_message = \Drupal::translation()->formatPlural(count($error_filters), $singular, $plural, [
+        '%entity-embed-filter-label' => $get_filter_label('entity_embed'),
+        '%filter' => reset($error_filters),
+        '%filters' => implode(', ', $error_filters),
+      ]);
+
+      $form_state->setErrorByName('filters', $error_message);
+    }
+  }
+}
+
+/**
+ * Implements hook_field_widget_form_alter().
+ */
+function entity_embed_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
+  // Add a `data-entity_embed-host-entity-langcode` attribute so that
+  // entity_embed's JavaScript can pass the host entity's language to
+  // EntityEmbedDialog, allowing it to present entities in the same language.
+  if (!empty($element['#type']) && $element['#type'] == 'text_format') {
+    if (!empty($context['items']) && $context['items'] instanceof FieldItemListInterface) {
+      $element['#attributes']['data-entity_embed-host-entity-langcode'] = $context['items']->getLangcode();
+    }
+    else {
+      $entity = $form_state->getFormObject()->getEntity();
+      if ($entity instanceof ContentEntityInterface) {
+        $element['#attributes']['data-entity_embed-host-entity-langcode'] = $entity->language()->getId();
+      }
+    }
+  }
+}
diff --git a/web/modules/entity_embed/entity_embed.routing.yml b/web/modules/entity_embed/entity_embed.routing.yml
index 0c61eed6ed..57b67103c4 100644
--- a/web/modules/entity_embed/entity_embed.routing.yml
+++ b/web/modules/entity_embed/entity_embed.routing.yml
@@ -5,5 +5,10 @@ entity_embed.dialog:
     _title: 'Embed entity'
   requirements:
     _embed_button_editor_access: 'TRUE'
-  options:
-    _theme: ajax_base_page
+
+entity_embed.preview:
+  path: '/entity-embed/preview/{filter_format}'
+  defaults:
+    _controller: '\Drupal\entity_embed\Controller\PreviewController::preview'
+  requirements:
+    _entity_access: 'filter_format.use'
diff --git a/web/modules/entity_embed/entity_embed.services.yml b/web/modules/entity_embed/entity_embed.services.yml
index 076de85fbb..9202c46bff 100644
--- a/web/modules/entity_embed/entity_embed.services.yml
+++ b/web/modules/entity_embed/entity_embed.services.yml
@@ -4,7 +4,7 @@ services:
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
   entity_embed.twig.entity_embed_twig_extension:
     class: Drupal\entity_embed\Twig\EntityEmbedTwigExtension
-    arguments: ['@entity.manager', '@entity_embed.builder']
+    arguments: ['@entity_type.manager', '@entity_embed.builder']
     tags:
       - { name: twig.extension }
   entity_embed.builder:
diff --git a/web/modules/entity_embed/js/plugins/drupalentity/plugin.js b/web/modules/entity_embed/js/plugins/drupalentity/plugin.js
index b8a5090c97..aa620e6de0 100644
--- a/web/modules/entity_embed/js/plugins/drupalentity/plugin.js
+++ b/web/modules/entity_embed/js/plugins/drupalentity/plugin.js
@@ -3,27 +3,98 @@
  * Drupal Entity embed plugin.
  */
 
-(function ($, Drupal, CKEDITOR) {
+(function (jQuery, Drupal, CKEDITOR) {
 
   "use strict";
 
+  function getFocusedWidget(editor) {
+    var widget = editor.widgets.focused;
+
+    if (widget && widget.name === 'drupalentity') {
+      return widget;
+    }
+
+    return null;
+  }
+
+  function linkCommandIntegrator(editor) {
+    if (!editor.plugins.drupallink) {
+      return;
+    }
+
+    editor.getCommand('drupalunlink').on('exec', function (evt) {
+      var widget = getFocusedWidget(editor);
+
+      if (!widget) {
+        return;
+      }
+
+      widget.setData('link', null);
+
+      this.refresh(editor, editor.elementPath());
+
+      evt.cancel();
+    });
+
+    editor.getCommand('drupalunlink').on('refresh', function (evt) {
+      var widget = getFocusedWidget(editor);
+
+      if (!widget) {
+        return;
+      }
+
+      this.setState(widget.data.link ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
+
+      evt.cancel();
+    });
+  }
+
   CKEDITOR.plugins.add('drupalentity', {
-    // This plugin requires the Widgets System defined in the 'widget' plugin.
     requires: 'widget',
 
-    // The plugin initialization logic goes inside this method.
     beforeInit: function (editor) {
       // Configure CKEditor DTD for custom drupal-entity element.
       // @see https://www.drupal.org/node/2448449#comment-9717735
       var dtd = CKEDITOR.dtd, tagName;
       dtd['drupal-entity'] = {'#': 1};
-      // Register drupal-entity element as allowed child, in each tag that can
+      // Register drupal-entity element as an allowed child in each tag that can
       // contain a div element.
       for (tagName in dtd) {
         if (dtd[tagName].div) {
           dtd[tagName]['drupal-entity'] = 1;
         }
       }
+      dtd['a']['drupal-entity'] = 1;
+
+      // The drupallink plugin has a hardcoded integration with
+      // drupalimage.  If the drupallink plugin has the
+      // registerLinkableWidget() method (which was added in Drupal 8.8), we
+      // don't need this workaround.
+      if (editor.plugins.drupallink) {
+        if (CKEDITOR.plugins.drupallink.hasOwnProperty('registerLinkableWidget')) {
+          CKEDITOR.plugins.drupallink.registerLinkableWidget('drupalentity');
+        } else {
+          // drupallink has a hardcoded integration with drupalimage. Workaround
+          // that, to reuse the same integration.
+          var originalGetFocusedWidget = null;
+          if (CKEDITOR.plugins.drupalimage) {
+            originalGetFocusedWidget = CKEDITOR.plugins.drupalimage.getFocusedWidget;
+          } else {
+            CKEDITOR.plugins.drupalimage = {};
+          }
+          CKEDITOR.plugins.drupalimage.getFocusedWidget = function () {
+            var ourFocusedWidget = getFocusedWidget(editor);
+            if (ourFocusedWidget) {
+              return ourFocusedWidget;
+            }
+            // If drupalimage is loaded, call that next, to not break its link command integration.
+            if (originalGetFocusedWidget) {
+              return originalGetFocusedWidget(editor);
+            }
+            return null;
+          };
+        }
+      }
 
       // Generic command for adding/editing entities of all types.
       editor.addCommand('editdrupalentity', {
@@ -35,20 +106,18 @@
           data = data || {};
 
           var existingElement = getSelectedEmbeddedEntity(editor);
+          var existingWidget = (existingElement) ? editor.widgets.getByElement(existingElement, true) : null;
 
           var existingValues = {};
-          if (existingElement && existingElement.$ && existingElement.$.firstChild) {
-            var embedDOMElement = existingElement.$.firstChild;
-            // Populate array with the entity's current attributes.
-            var attribute = null, attributeName;
-            for (var key = 0; key < embedDOMElement.attributes.length; key++) {
-              attribute = embedDOMElement.attributes.item(key);
-              attributeName = attribute.nodeName.toLowerCase();
-              if (attributeName.substring(0, 15) === 'data-cke-saved-') {
-                continue;
-              }
-              existingValues[attributeName] = existingElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
-            }
+
+          // Host entity's langcode added in entity_embed_field_widget_form_alter().
+          var hostEntityLangcode = document.getElementById(editor.name).getAttribute('data-entity_embed-host-entity-langcode');
+          if (hostEntityLangcode) {
+            existingValues['data-langcode'] = hostEntityLangcode;
+          }
+
+          if (existingWidget) {
+            existingValues = existingWidget.data.attributes;
           }
 
           var embed_button_id = data.id ? data.id : existingValues['data-embed-button'];
@@ -59,19 +128,24 @@
           };
 
           var saveCallback = function (values) {
-            var entityElement = editor.document.createElement('drupal-entity');
-            var attributes = values.attributes;
-            for (var key in attributes) {
-              entityElement.setAttribute(key, attributes[key]);
+            editor.fire('saveSnapshot');
+            if (!existingElement) {
+              var entityElement = editor.document.createElement('drupal-entity');
+              var attributes = values.attributes;
+              for (var key in attributes) {
+                entityElement.setAttribute(key, attributes[key]);
+              }
+              editor.insertHtml(entityElement.getOuterHtml());
             }
-
-            editor.insertHtml(entityElement.getOuterHtml());
-            if (existingElement) {
-              // Detach the behaviors that were attached when the entity content
-              // was inserted.
-              Drupal.runEmbedBehaviors('detach', existingElement.$);
-              existingElement.remove();
+            else {
+              var hasCaption = false;
+              if (values.attributes['data-caption']) {
+                values.attributes['data-caption'] = CKEDITOR.tools.htmlDecodeAttr(values.attributes['data-caption']);
+                hasCaption = true;
+              }
+              existingWidget.setData({ attributes: values.attributes, hasCaption: hasCaption });
             }
+            editor.fire('saveSnapshot');
           };
 
           // Open the entity embed dialog for corresponding EmbedButton.
@@ -85,49 +159,192 @@
         allowedContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]',
         requiredContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]',
 
-        // Simply recognize the element as our own. The inner markup if fetched
-        // and inserted the init() callback, since it requires the actual DOM
-        // element.
-        upcast: function (element) {
+        pathName: Drupal.t('Embedded entity'),
+
+        editables: {
+          caption: {
+            selector: 'figcaption',
+            allowedContent: 'a[!href]; em strong cite code br',
+            pathName: Drupal.t('Caption'),
+          }
+        },
+
+        upcast: function (element, data) {
           var attributes = element.attributes;
-          if (attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) {
+          if (element.name !== 'drupal-entity' || attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) {
             return;
           }
-          // Generate an ID for the element, so that we can use the Ajax
-          // framework.
-          element.attributes.id = generateEmbedId();
+          data.attributes = CKEDITOR.tools.copy(attributes);
+          data.hasCaption = data.attributes.hasOwnProperty('data-caption');
+          data.link = null;
+          if (element.parent.name === 'a') {
+            data.link = CKEDITOR.tools.copy(element.parent.attributes);
+            // Omit CKEditor-internal attributes.
+            Object.keys(element.parent.attributes).forEach(function (attrName) {
+              if (attrName.indexOf('data-cke-') !== -1) {
+                delete data.link[attrName];
+              }
+            });
+          }
           return element;
         },
 
-        // Fetch the rendered entity.
         init: function () {
           /** @type {CKEDITOR.dom.element} */
           var element = this.element;
-          // Use the Ajax framework to fetch the HTML, so that we can retrieve
-          // out-of-band assets (JS, CSS...).
-          var entityEmbedPreview = Drupal.ajax({
-            base: element.getId(),
-            element: element.$,
-            url: Drupal.url('embed/preview/' + editor.config.drupal.format + '?' + $.param({
-              value: element.getOuterHtml()
-            })),
-            progress: {type: 'none'},
-            // Use a custom event to trigger the call.
-            event: 'entity_embed_dummy_event'
-          });
-          entityEmbedPreview.execute();
+
+          // See https://www.drupal.org/node/2544018.
+          if (element.hasAttribute('data-embed-button')) {
+            var buttonId = element.getAttribute('data-embed-button');
+            if (editor.config.DrupalEntity_buttons[buttonId]) {
+              var button = editor.config.DrupalEntity_buttons[buttonId];
+              this.wrapper.data('cke-display-name', Drupal.t('Embedded @buttonLabel', {'@buttonLabel': button.label}));
+            }
+          }
         },
 
-        // Downcast the element.
-        downcast: function (element) {
-          // Only keep the wrapping element.
-          element.setHtml('');
-          // Remove the auto-generated ID.
-          delete element.attributes.id;
-          return element;
+        destroy: function() {
+          this._tearDownDynamicEditables();
+        },
+
+        data: function (event) {
+          if (this._previewNeedsServersideUpdate()) {
+            editor.fire('lockSnapshot');
+            this._tearDownDynamicEditables();
+
+            this._loadPreview(function (widget) {
+              widget._setUpDynamicEditables();
+              editor.fire('unlockSnapshot');
+            });
+          }
+          // @todo Remove in https://www.drupal.org/project/entity_embed/issues/3060397
+          else if (this._previewNeedsClientsideUpdate()) {
+            this._performClientsideUpdate();
+            editor.fire('saveSnapshot');
+          }
+
+          // Allow entity_embed.editor.css to respond to changes (for example in alignment).
+          this.element.setAttributes(this.data.attributes);
+
+          // Track the previous state, to allow for smarter decisions.
+          this.oldData = CKEDITOR.tools.clone(this.data);
+        },
+
+        downcast: function () {
+          var downcastElement = new CKEDITOR.htmlParser.element('drupal-entity', this.data.attributes);
+          if (this.data.link) {
+            var link = new CKEDITOR.htmlParser.element('a', this.data.link);
+            link.add(downcastElement);
+            downcastElement = link;
+          }
+          return downcastElement;
+        },
+
+        _setUpDynamicEditables: function () {
+          // Now that the caption is available in the DOM, make it editable.
+          if (this.initEditable('caption', this.definition.editables.caption)) {
+            var captionEditable = this.editables.caption;
+            // @see core/modules/filter/css/filter.caption.css
+            // @see ckeditor_ckeditor_css_alter()
+            captionEditable.setAttribute('data-placeholder', Drupal.t('Enter caption here'));
+            // And ensure that any changes made to it are persisted.
+            var config = {characterData: true, attributes: true, childList: true, subtree: true};
+            var widget = this;
+            this.captionEditableMutationObserver = new MutationObserver(function () {
+              var entityAttributes = CKEDITOR.tools.clone(widget.data.attributes);
+              entityAttributes['data-caption'] = captionEditable.getData();
+              widget.setData('attributes', entityAttributes);
+            });
+            this.captionEditableMutationObserver.observe(captionEditable.$, config);
+          }
+        },
+
+        _tearDownDynamicEditables: function () {
+          if (this.captionEditableMutationObserver) {
+            this.captionEditableMutationObserver.disconnect();
+          }
+        },
+
+        _previewNeedsServersideUpdate: function () {
+          // When the widget is first loading, it of course needs to still get a preview!
+          if (!this.ready) {
+            return true;
+          }
+
+          return this._hashData(this.oldData) !== this._hashData(this.data);
+        },
+
+        // @todo Remove in https://www.drupal.org/project/entity_embed/issues/3060397
+        _previewNeedsClientsideUpdate: function () {
+          // The preview's caption must be updated when the caption was edited in EntityEmbedDialog.
+          // @see https://www.drupal.org/project/entity_embed/issues/3060397
+          if (this.data.hasCaption && this.editables.caption.getData() !== this.data.attributes['data-caption']) {
+            return true;
+          }
+
+          return false;
+        },
+
+        // @todo Remove in https://www.drupal.org/project/entity_embed/issues/3060397
+        _performClientsideUpdate: function () {
+          if (this.data.hasCaption) {
+            this.captionEditableMutationObserver.disconnect();
+            this.editables.caption.$.innerHTML = this.data.attributes['data-caption'];
+            var config = {characterData: true, attributes: false, childList: true, subtree: true};
+            this.captionEditableMutationObserver.observe(this.editables.caption.$, config);
+          }
+        },
+
+        /**
+         * Computes a hash of the data that can only be previewed by the server.
+         */
+        _hashData: function (data) {
+          var dataToHash = CKEDITOR.tools.clone(data);
+          // The caption does not need rendering.
+          if (dataToHash.attributes.hasOwnProperty('data-caption')) {
+            delete dataToHash.attributes['data-caption'];
+          }
+          // Changed link destinations do not affect the visual preview.
+          if (dataToHash.link && dataToHash.link.hasOwnProperty('href')) {
+            delete dataToHash.link.href;
+          }
+          return JSON.stringify(dataToHash);
+        },
+
+        /**
+         * Loads an entity embed preview, calls a callback after insertion.
+         *
+         * @param {function} callback
+         *   A callback function that will be called after the preview has loaded, and receives the widget instance.
+         */
+        _loadPreview: function (callback) {
+          var widget = this;
+          jQuery.get({
+            url: Drupal.url('entity-embed/preview/' + editor.config.drupal.format + '?text=' + encodeURIComponent(this.downcast().getOuterHtml())),
+            dataType: 'html',
+          }).done(function(previewHtml) {
+            widget.element.setHtml(previewHtml);
+            callback(widget);
+          });
         }
       });
 
+      editor.widgets.on('instanceCreated', function (event) {
+        var widget = event.data;
+
+        if (widget.name !== 'drupalentity') {
+          return;
+        }
+
+        widget.on('edit', function (event) {
+          event.cancel();
+          // @see https://www.drupal.org/node/2544018
+          if (isEditableEntityWidget(editor, event.sender.wrapper)) {
+            editor.execCommand('editdrupalentity');
+          }
+        });
+      });
+
       // Register the toolbar buttons.
       if (editor.ui.addButton) {
         for (var key in editor.config.DrupalEntity_buttons) {
@@ -135,40 +352,46 @@
           editor.ui.addButton(button.id, {
             label: button.label,
             data: button,
-            allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button]',
+            allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button,!data-langcode,!alt,!title]',
             click: function(editor) {
               editor.execCommand('editdrupalentity', this.data);
             },
-            icon: button.image
+            icon: button.image,
+            modes: {wysiwyg: 1, source: 0}
           });
         }
       }
 
-      // Register context menu option for editing widget.
+      // Register context menu items for editing widget.
       if (editor.contextMenu) {
         editor.addMenuGroup('drupalentity');
-        editor.addMenuItem('drupalentity', {
-          label: Drupal.t('Edit Entity'),
-          icon: this.path + 'entity.png',
-          command: 'editdrupalentity',
-          group: 'drupalentity'
-        });
+
+        for (var key in editor.config.DrupalEntity_buttons) {
+          var button = editor.config.DrupalEntity_buttons[key];
+
+          var label = Drupal.t('Edit @buttonLabel', { '@buttonLabel': button.label });
+
+          editor.addMenuItem('drupalentity_' + button.id, {
+            label: label,
+            icon: button.image,
+            command: 'editdrupalentity',
+            group: 'drupalentity'
+          });
+        }
 
         editor.contextMenu.addListener(function(element) {
           if (isEditableEntityWidget(editor, element)) {
-            return { drupalentity: CKEDITOR.TRISTATE_OFF };
+            var button_id = element.getFirst().getAttribute('data-embed-button');
+            var returnData = {};
+            returnData['drupalentity_' + button_id] = CKEDITOR.TRISTATE_OFF;
+            return returnData;
           }
         });
       }
+    },
 
-      // Execute widget editing action on double click.
-      editor.on('doubleclick', function (evt) {
-        var element = getSelectedEmbeddedEntity(editor) || evt.data.element;
-
-        if (isEditableEntityWidget(editor, element)) {
-          editor.execCommand('editdrupalentity');
-        }
-      });
+    afterInit: function (editor) {
+      linkCommandIntegrator(editor);
     }
   });
 
@@ -199,7 +422,7 @@
       return false;
     }
 
-    var button = $(element.$.firstChild).attr('data-embed-button');
+    var button = element.$.firstChild.getAttribute('data-embed-button');
     if (!button) {
       // If there was no data-embed-button attribute, not editable.
       return false;
@@ -209,16 +432,4 @@
     return editor.config.DrupalEntity_buttons.hasOwnProperty(button);
   }
 
-  /**
-   * Generates unique HTML IDs for the widgets.
-   *
-   * @returns {string}
-   */
-  function generateEmbedId() {
-    if (typeof generateEmbedId.counter == 'undefined') {
-      generateEmbedId.counter = 0;
-    }
-    return 'entity-embed-' + generateEmbedId.counter++;
-  }
-
 })(jQuery, Drupal, CKEDITOR);
diff --git a/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php b/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php
index 776ef82f69..3339088ca0 100644
--- a/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php
+++ b/web/modules/entity_embed/src/Annotation/EntityEmbedDisplay.php
@@ -32,9 +32,9 @@ class EntityEmbedDisplay extends Plugin {
   /**
    * The human-readable name of the Entity Embed Display plugin.
    *
-   * @ingroup plugin_translatable
-   *
    * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
    */
   public $label = '';
 
@@ -55,4 +55,14 @@ class EntityEmbedDisplay extends Plugin {
    */
   public $no_ui = FALSE;
 
+  /**
+   * Alt and title access.
+   *
+   * Whether the plugin supports per-embed alt and title overrides for media
+   * entities with an image source.
+   *
+   * @var bool
+   */
+  public $supports_image_alt_and_title = FALSE;
+
 }
diff --git a/web/modules/entity_embed/src/Controller/PreviewController.php b/web/modules/entity_embed/src/Controller/PreviewController.php
new file mode 100644
index 0000000000..33102d9a14
--- /dev/null
+++ b/web/modules/entity_embed/src/Controller/PreviewController.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\entity_embed\Controller;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\filter\FilterFormatInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Controller which renders a preview of the provided text.
+ */
+class PreviewController implements ContainerInjectionInterface {
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs an PreviewController instance.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(RendererInterface $renderer) {
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * Returns a HTML response containing a preview of the text after filtering.
+   *
+   * Applies all of the given text format's filters, not just the `entity_embed`
+   * filter, because for example `filter_align` and `filter_caption` may apply
+   * to it as well.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param \Drupal\filter\FilterFormatInterface $filter_format
+   *   The text format.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The filtered text.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   *   Throws an exception if 'text' parameter is not found in the request.
+   *
+   * @see \Drupal\editor\EditorController::getUntransformedText
+   */
+  public function preview(Request $request, FilterFormatInterface $filter_format) {
+    $text = $request->get('text');
+    if ($text == '') {
+      throw new NotFoundHttpException();
+    }
+
+    $build = [
+      '#type' => 'processed_text',
+      '#text' => $text,
+      '#format' => $filter_format->id(),
+    ];
+    $html = $this->renderer->renderPlain($build);
+
+    // Note that we intentionally do not use:
+    // - \Drupal\Core\Cache\CacheableResponse because caching it on the server
+    //   side is wasteful, hence there is no need for cacheability metadata.
+    // - \Drupal\Core\Render\HtmlResponse because there is no need for
+    //   attachments nor cacheability metadata.
+    return (new Response($html))
+      // Do not allow any intermediary to cache the response, only the end user.
+      ->setPrivate()
+      // Allow the end user to cache it for up to 5 minutes.
+      ->setMaxAge(300);
+  }
+
+}
diff --git a/web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php b/web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php
new file mode 100644
index 0000000000..34241cc61d
--- /dev/null
+++ b/web/modules/entity_embed/src/Entity/EntityEmbedFakeEntity.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\entity_embed\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+
+/**
+ * Fake entity type.
+ *
+ * @ContentEntityType(
+ *   id = "entity_embed_fake_entity",
+ *   label = @Translation("Fake entity type"),
+ *   handlers = {
+ *     "storage" = "Drupal\Core\Entity\ContentEntityNullStorage",
+ *   },
+ * )
+ */
+class EntityEmbedFakeEntity extends ContentEntityBase {
+}
diff --git a/web/modules/entity_embed/src/EntityEmbedBuilder.php b/web/modules/entity_embed/src/EntityEmbedBuilder.php
index 24e716afbb..892ec3acb5 100644
--- a/web/modules/entity_embed/src/EntityEmbedBuilder.php
+++ b/web/modules/entity_embed/src/EntityEmbedBuilder.php
@@ -34,6 +34,7 @@ class EntityEmbedBuilder implements EntityEmbedBuilderInterface {
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler service.
    * @param \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager $display_manager
+   *   The entity embed display plugin manager.
    */
   public function __construct(ModuleHandlerInterface $module_handler, EntityEmbedDisplayManager $display_manager) {
     $this->moduleHandler = $module_handler;
@@ -58,6 +59,18 @@ public function buildEntityEmbed(EntityInterface $entity, array $context = []) {
       'data-entity-embed-display-settings' => [],
     ];
 
+    // If the data-entity-embed-display-settings isn't an array reset  it,
+    // otherwise we'll encounter a fatal error when calling
+    // $this->buildEntityEmbedDisplayPlugin() further down the line.
+    if (!is_array($context['data-entity-embed-display-settings'])) {
+      \Drupal::logger('entity_embed')->warning('Invalid display settings encountered. Could not process following settings for entity type "@entity_type" with the uuid "@uuid": @settings', [
+        '@settings' => $context['data-entity-embed-display-settings'],
+        '@entity_type' => $entity->getEntityTypeId(),
+        '@uuid' => $entity->uuid(),
+      ]);
+      $context['data-entity-embed-display-settings'] = [];
+    }
+
     // The default Entity Embed Display plugin has been deprecated by the
     // rendered entity field formatter.
     if ($context['data-entity-embed-display'] === 'default') {
@@ -76,7 +89,6 @@ public function buildEntityEmbed(EntityInterface $entity, array $context = []) {
     // alter the result before rendering.
     $build = [
       '#theme_wrappers' => ['entity_embed_container'],
-      '#attributes' => ['class' => ['embedded-entity']],
       '#entity' => $entity,
       '#context' => $context,
     ];
@@ -86,18 +98,25 @@ public function buildEntityEmbed(EntityInterface $entity, array $context = []) {
       $context['data-entity-embed-display-settings'],
       $context
     );
-
-    // Maintain data-align if it is there.
-    if (isset($context['data-align'])) {
-      $build['#attributes']['data-align'] = $context['data-align'];
+    // Don't ever cache a representation of an embedded entity, since the host
+    // entity may be overriding specific values (such as an `alt` attribute)
+    // which means that this particular rendered representation is unique to
+    // the host entity, and hence nonsensical to cache separately anyway.
+    unset($build['entity']['#cache']['keys']);
+
+    if (isset($context['class'])) {
+      if (is_string($context['class'])) {
+        $context['class'] = explode(' ', $context['class']);
+      }
     }
-    elseif ((isset($context['class']))) {
-      $build['#attributes']['class'][] = $context['class'];
+    else {
+      $context['class'] = [];
     }
+    $context['class'][] = 'embedded-entity';
 
-    // Maintain data-caption if it is there.
-    if (isset($context['data-caption'])) {
-      $build['#attributes']['data-caption'] = $context['data-caption'];
+    // Maintain data- attributes.
+    if (isset($context)) {
+      $build['#attributes'] = $context;
     }
 
     // Make sure that access to the entity is respected.
diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php
index ca0a41277c..3cb86cdbac 100644
--- a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php
+++ b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php
@@ -35,7 +35,7 @@ abstract class EntityEmbedDisplayBase extends PluginBase implements ContainerFac
   /**
    * The language manager.
    *
-   * @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   * @var \Drupal\Core\Language\LanguageManagerInterface
    */
   protected $languageManager;
 
@@ -44,18 +44,24 @@ abstract class EntityEmbedDisplayBase extends PluginBase implements ContainerFac
    *
    * @var array
    */
-  public $context = array();
+  public $context = [];
 
   /**
    * The attributes on the embedded entity.
    *
    * @var array
    */
-  public $attributes = array();
+  public $attributes = [];
 
   /**
-   * {@inheritdoc}
+   * Constructs an EntityEmbedDisplayBase object.
    *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager service.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
@@ -94,8 +100,7 @@ public function access(AccountInterface $account = NULL) {
   }
 
   /**
-   * Validates that this Entity Embed Display plugin applies to the current
-   * entity type.
+   * Validates that this display plugin applies to the current entity type.
    *
    * This checks the plugin annotation's 'entity_types' value, which should be
    * an array of entity types that this plugin can process, or FALSE if the
@@ -131,14 +136,14 @@ abstract public function build();
    * {@inheritdoc}
    */
   public function calculateDependencies() {
-    return array();
+    return [];
   }
 
   /**
    * {@inheritdoc}
    */
   public function defaultConfiguration() {
-    return array();
+    return [];
   }
 
   /**
@@ -232,7 +237,7 @@ public function getContextValues() {
    *   The currently set context value.
    */
   public function getContextValue($name) {
-    return $this->context[$name];
+    return !empty($this->context[$name]) ? $this->context[$name] : NULL;
   }
 
   /**
@@ -266,7 +271,7 @@ public function getEntityTypeFromContext() {
   /**
    * Gets the entity from the current context.
    *
-   * @todo Where doe sthis come from? The value must come from somewhere, yet
+   * @todo Where does this come from? The value must come from somewhere, yet
    * this does not implement any context-related interfaces. This is an *input*,
    * so we need cache contexts and possibly cache tags to reflect where this
    * came from. We need that for *everything* that this class does that relies
@@ -274,6 +279,7 @@ public function getEntityTypeFromContext() {
    * global that breaks cacheability metadata.
    *
    * @return \Drupal\Core\Entity\EntityInterface
+   *   The entity from the current context.
    */
   public function getEntityFromContext() {
     if ($this->hasContextValue('entity')) {
@@ -317,10 +323,25 @@ public function getAttributeValue($name, $default = NULL) {
     return array_key_exists($name, $attributes) ? $attributes[$name] : $default;
   }
 
+  /**
+   * Checks if an attribute is set.
+   *
+   * @param string $name
+   *   The name of the attribute.
+   *
+   * @return bool
+   *   Returns TRUE if value is set.
+   */
+  public function hasAttribute($name) {
+    return array_key_exists($name, $this->getAttributeValues());
+  }
+
   /**
    * Gets the current language code.
    *
    * @return string
+   *   The langcode present in the 'data-langcode', if present, or the current
+   *   langcode from the language manager, otherwise.
    */
   public function getLangcode() {
     $langcode = $this->getAttributeValue('data-langcode');
diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php
index 3d97fac2cd..f35f28b0f3 100644
--- a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php
+++ b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayInterface.php
@@ -3,9 +3,10 @@
 namespace Drupal\entity_embed\EntityEmbedDisplay;
 
 use Drupal\Component\Plugin\PluginInspectionInterface;
-use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\DependentPluginInterface;
 
 /**
  * Defines the interface for Entity Embed Display plugins.
@@ -41,7 +42,7 @@
  *
  * @ingroup entity_embed_api
  */
-interface EntityEmbedDisplayInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface {
+interface EntityEmbedDisplayInterface extends ConfigurableInterface, DependentPluginInterface, PluginFormInterface, PluginInspectionInterface {
 
   /**
    * Indicates whether this Entity Embed display can be used.
diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php
index f2d9d69b3f..67531e2399 100644
--- a/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php
+++ b/web/modules/entity_embed/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
 use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\MediaImageDecorator;
 
 /**
  * Provides an Entity Embed display plugin manager.
@@ -32,20 +33,8 @@ class EntityEmbedDisplayManager extends DefaultPluginManager {
   public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
     parent::__construct('Plugin/entity_embed/EntityEmbedDisplay', $namespaces, $module_handler, 'Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface', 'Drupal\entity_embed\Annotation\EntityEmbedDisplay');
     $this->alterInfo('entity_embed_display_plugins');
-    $this->setCacheBackend($cache_backend, 'entity_embed_display_plugins');
-  }
-
-  /**
-   * Overrides DefaultPluginManager::processDefinition().
-   */
-  public function processDefinition(&$definition, $plugin_id) {
-    $definition += array(
-      'entity_types' => FALSE,
-    );
-
-    if ($definition['entity_types'] !== FALSE && !is_array($definition['entity_types'])) {
-      $definition['entity_types'] = array($definition['entity_types']);
-    }
+    // @todo Move the cache tag to the derivers once https://www.drupal.org/node/3001284 lands.
+    $this->setCacheBackend($cache_backend, 'entity_embed_display_plugins', ['config:entity_view_mode_list']);
   }
 
   /**
@@ -61,8 +50,22 @@ public function processDefinition(&$definition, $plugin_id) {
    *
    * @see https://drupal.org/node/2277981
    */
-  public function getDefinitionsForContexts(array $contexts = array()) {
+  public function getDefinitionsForContexts(array $contexts = []) {
     $definitions = $this->getDefinitions();
+
+    if (!empty($contexts['embed_button'])) {
+      $button_plugins = $contexts['embed_button']->getTypeSetting('display_plugins');
+      if (!empty($button_plugins)) {
+        $allowed_definitions = [];
+        foreach ($button_plugins as $plugin_id) {
+          if (!empty($definitions[$plugin_id])) {
+            $allowed_definitions[$plugin_id] = $definitions[$plugin_id];
+          }
+        }
+        $definitions = $allowed_definitions;
+      }
+    }
+
     $valid_ids = array_filter(array_keys($definitions), function ($id) use ($contexts) {
       try {
         $display = $this->createInstance($id);
@@ -82,6 +85,35 @@ public function getDefinitionsForContexts(array $contexts = array()) {
     return $definitions_for_context;
   }
 
+  /**
+   * Gets definition options for context.
+   *
+   * Provides a list of plugins that can be used for a certain context and
+   * filters out plugins that should be hidden in the UI.
+   *
+   * @param array $context
+   *   An array of context options; possible keys are 'entity', 'entity_type'
+   *   and 'embed_button'.
+   *
+   * @return string[]
+   *   An array of valid plugin labels, keyed by plugin ID.
+   */
+  public function getDefinitionOptionsForContext(array $context) {
+    $values = [
+      'entity' => TRUE,
+      'entity_type' => TRUE,
+      'embed_button' => TRUE,
+    ];
+    assert(empty(array_diff_key($context, $values)));
+    $definitions = $this->getDefinitionsForContexts($context);
+    $definitions = $this->filterExposedDefinitions($definitions);
+    $options = array_map(function ($definition) {
+      return (string) $definition['label'];
+    }, $definitions);
+    natsort($options);
+    return $options;
+  }
+
   /**
    * Gets definition options for entity.
    *
@@ -95,7 +127,7 @@ public function getDefinitionsForContexts(array $contexts = array()) {
    *   An array of valid plugin labels, keyed by plugin ID.
    */
   public function getDefinitionOptionsForEntity(EntityInterface $entity) {
-    $definitions = $this->getDefinitionsForContexts(array('entity' => $entity, 'entity_type' => $entity->getEntityTypeId()));
+    $definitions = $this->getDefinitionsForContexts(['entity' => $entity, 'entity_type' => $entity->getEntityTypeId()]);
     $definitions = $this->filterExposedDefinitions($definitions);
     return array_map(function ($definition) {
       return (string) $definition['label'];
@@ -112,7 +144,7 @@ public function getDefinitionOptionsForEntity(EntityInterface $entity) {
    *   Returns plugin definitions that should be displayed in the UI.
    */
   protected function filterExposedDefinitions(array $definitions) {
-    return array_filter($definitions, function($definition) {
+    return array_filter($definitions, function ($definition) {
       return empty($definition['no_ui']);
     });
   }
@@ -130,11 +162,28 @@ protected function filterExposedDefinitions(array $definitions) {
    *   An array of valid plugin labels, keyed by plugin ID.
    */
   public function getDefinitionOptionsForEntityType($entity_type) {
-    $definitions = $this->getDefinitionsForContexts(array('entity_type' => $entity_type));
+    $definitions = $this->getDefinitionsForContexts(['entity_type' => $entity_type]);
     $definitions = $this->filterExposedDefinitions($definitions);
     return array_map(function ($definition) {
       return (string) $definition['label'];
     }, $definitions);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createInstance($plugin_id, array $configuration = []) {
+    $instance = parent::createInstance($plugin_id, $configuration);
+    $definition = $instance->getPluginDefinition();
+
+    if (empty($definition['supports_image_alt_and_title'])) {
+      return $instance;
+    }
+    else {
+      // Use decorator pattern to add alt and title fields to dialog when
+      // embedding media with image source.
+      return new MediaImageDecorator($instance);
+    }
+  }
+
 }
diff --git a/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php b/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php
index fd80bf8544..038bba2435 100644
--- a/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php
+++ b/web/modules/entity_embed/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php
@@ -11,11 +11,11 @@
 use Drupal\Core\Plugin\PluginDependencyTrait;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\TypedDataManager;
-use Drupal\node\Entity\Node;
+use Drupal\entity_embed\Entity\EntityEmbedFakeEntity;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- *
+ * Base class for field formatter display plugins.
  */
 abstract class FieldFormatterEntityEmbedDisplayBase extends EntityEmbedDisplayBase {
   use PluginDependencyTrait;
@@ -51,6 +51,12 @@ abstract class FieldFormatterEntityEmbedDisplayBase extends EntityEmbedDisplayBa
   /**
    * Constructs a FieldFormatterEntityEmbedDisplayBase object.
    *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager service.
    * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager
@@ -142,9 +148,9 @@ public function getFieldFormatterId() {
    * {@inheritdoc}
    */
   public function build() {
-    // Create a temporary node object to which our fake field value can be
+    // Create a temporary entity to which our fake field value can be
     // added.
-    $node = Node::create(array('type' => '_entity_embed'));
+    $fakeEntity = EntityEmbedFakeEntity::create(['type' => '_entity_embed']);
 
     $definition = $this->getFieldDefinition();
 
@@ -156,12 +162,12 @@ public function build() {
       $definition,
       $this->getFieldValue($definition),
       $definition->getName(),
-      $node->getTypedData()
+      $fakeEntity->getTypedData()
     );
 
     // Prepare, expects an array of items, keyed by parent entity ID.
     $formatter = $this->getFieldFormatter();
-    $formatter->prepareView(array($node->id() => $items));
+    $formatter->prepareView([$fakeEntity->id() => $items]);
     $build = $formatter->viewElements($items, $this->getLangcode());
     // For some reason $build[0]['#printed'] is TRUE, which means it will fail
     // to render later. So for now we manually fix that.
@@ -192,20 +198,20 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    */
   public function getFieldFormatter() {
     if (!isset($this->fieldFormatter)) {
-      $display = array(
+      $display = [
         'type' => $this->getFieldFormatterId(),
         'settings' => $this->getConfiguration(),
         'label' => 'hidden',
-      );
+      ];
 
       // Create the formatter plugin. Will use the default formatter for that
       // field type if none is passed.
       $this->fieldFormatter = $this->formatterPluginManager->getInstance(
-        array(
+        [
           'field_definition' => $this->getFieldDefinition(),
           'view_mode' => '_entity_embed',
           'configuration' => $display,
-        )
+        ]
       );
     }
 
diff --git a/web/modules/entity_embed/src/Form/EntityEmbedDialog.php b/web/modules/entity_embed/src/Form/EntityEmbedDialog.php
index fe16bd97c8..cc9daaf6eb 100644
--- a/web/modules/entity_embed/src/Form/EntityEmbedDialog.php
+++ b/web/modules/entity_embed/src/Form/EntityEmbedDialog.php
@@ -3,15 +3,15 @@
 namespace Drupal\entity_embed\Form;
 
 use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\CloseModalDialogCommand;
 use Drupal\Core\Ajax\HtmlCommand;
 use Drupal\Core\Ajax\SetDialogTitleCommand;
-use Drupal\Core\Entity\Element\EntityAutocomplete;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\TranslatableInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormBuilderInterface;
@@ -82,6 +82,8 @@ class EntityEmbedDialog extends FormBase {
 
   /**
    * The entity browser settings from the entity embed button.
+   *
+   * @var array
    */
   protected $entityBrowserSettings = [];
 
@@ -120,7 +122,8 @@ public static function create(ContainerInterface $container) {
       $container->get('entity_type.manager'),
       $container->get('event_dispatcher'),
       $container->get('entity_field.manager'),
-      $container->get('module_handler')
+      $container->get('module_handler'),
+      $container->get('language_manager')
     );
   }
 
@@ -132,8 +135,35 @@ public function getFormId() {
   }
 
   /**
-   * {@inheritdoc}
+   * Loads an entity (in the appropriate translation) given HTML attributes.
    *
+   * @param string[] $attributes
+   *   An array of HTML attributes, including at least `data-entity-type` and
+   *   `data-entity-uuid`, and optionally `data-langcode`.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|null
+   *   The requested entity, or NULL.
+   */
+  protected function loadEntityByAttributes(array $attributes) {
+    $entity = $this->entityTypeManager->getStorage($attributes['data-entity-type'])
+      ->loadByProperties(['uuid' => $attributes['data-entity-uuid']]);
+    $entity = current($entity);
+    if ($entity && $entity instanceof TranslatableInterface && !empty($attributes['data-langcode'])) {
+      if ($entity->hasTranslation($attributes['data-langcode'])) {
+        $entity = $entity->getTranslation($attributes['data-langcode']);
+      }
+    }
+
+    return $entity;
+  }
+
+  /**
+   * Form constructor.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
    * @param \Drupal\editor\EditorInterface $editor
    *   The editor to which this dialog corresponds.
    * @param \Drupal\embed\EmbedButtonInterface $embed_button
@@ -147,12 +177,12 @@ public function buildForm(array $form, FormStateInterface $form_state, EditorInt
     $form_state->set('embed_button', $embed_button);
     $form_state->set('editor', $editor);
     // Initialize entity element with form attributes, if present.
-    $entity_element = empty($values['attributes']) ? array() : $values['attributes'];
-    $entity_element += empty($input['attributes']) ? array() : $input['attributes'];
+    $entity_element = empty($values['attributes']) ? [] : $values['attributes'];
+    $entity_element += empty($input['attributes']) ? [] : $input['attributes'];
     // The default values are set directly from \Drupal::request()->request,
     // provided by the editor plugin opening the dialog.
     if (!$form_state->get('entity_element')) {
-      $form_state->set('entity_element', isset($input['editor_object']) ? $input['editor_object'] : array());
+      $form_state->set('entity_element', isset($input['editor_object']) ? $input['editor_object'] : []);
     }
     $entity_element += $form_state->get('entity_element');
     $entity_element += [
@@ -162,9 +192,8 @@ public function buildForm(array $form, FormStateInterface $form_state, EditorInt
       'data-entity-embed-display-settings' => isset($form_state->get('entity_element')['data-entity-embed-settings']) ? $form_state->get('entity_element')['data-entity-embed-settings'] : [],
     ];
     $form_state->set('entity_element', $entity_element);
-    $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type'])
-      ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]);
-    $form_state->set('entity', current($entity) ?: NULL);
+    $entity = $this->loadEntityByAttributes($entity_element);
+    $form_state->set('entity', $entity ?: NULL);
 
     if (!$form_state->get('step')) {
       // If an entity has been selected, then always skip to the embed options.
@@ -209,30 +238,31 @@ public function buildForm(array $form, FormStateInterface $form_state, EditorInt
    * @return array
    *   The form structure.
    */
-  public function buildSelectStep(array &$form, FormStateInterface $form_state) {
-    // Entity element is calculated on every AJAX request/submit. See ::buildForm().
+  public function buildSelectStep(array $form, FormStateInterface $form_state) {
+    // Entity element is calculated on every AJAX request/submit.
+    // See self::buildForm().
     $entity_element = $form_state->get('entity_element');
     /** @var \Drupal\embed\EmbedButtonInterface $embed_button */
     $embed_button = $form_state->get('embed_button');
     $entity = $form_state->get('entity');
 
-    $form['attributes']['data-entity-type'] = array(
+    $form['attributes']['data-entity-type'] = [
       '#type' => 'value',
       '#value' => $entity_element['data-entity-type'],
-    );
+    ];
 
     $label = $this->t('Label');
     // Attempt to display a better label if we can by getting it from
     // the label field definition.
     $entity_type = $this->entityTypeManager->getDefinition($entity_element['data-entity-type']);
-    if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface') && $entity_type->hasKey('label')) {
+    if ($entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasKey('label')) {
       $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type->id());
       if (isset($field_definitions[$entity_type->getKey('label')])) {
         $label = $field_definitions[$entity_type->getKey('label')]->getLabel();
       }
     }
 
-    $form['#title'] = $this->t('Select @type to embed', array('@type' => $entity_type->getLowercaseLabel()));
+    $form['#title'] = $this->t('Select @type to embed', ['@type' => $entity_type->getSingularLabel()]);
 
     if ($this->entityBrowser) {
       $this->eventDispatcher->addListener(Events::REGISTER_JS_CALLBACKS, [$this, 'registerJSCallback']);
@@ -246,43 +276,51 @@ public function buildSelectStep(array &$form, FormStateInterface $form_state) {
       ];
     }
     else {
-      $form['entity_id'] = array(
+      $form['entity_id'] = [
         '#type' => 'entity_autocomplete',
         '#target_type' => $entity_element['data-entity-type'],
         '#title' => $label,
         '#default_value' => $entity,
         '#required' => TRUE,
         '#description' => $this->t('Type label and pick the right one from suggestions. Note that the unique ID will be saved.'),
-      );
+        '#maxlength' => 255,
+      ];
       if ($bundles = $embed_button->getTypeSetting('bundles')) {
         $form['entity_id']['#selection_settings']['target_bundles'] = $bundles;
       }
     }
 
-    $form['attributes']['data-entity-uuid'] = array(
+    if (!empty($entity_element['data-langcode'])) {
+      $form['attributes']['data-langcode'] = [
+        '#type' => 'hidden',
+        '#value' => $entity_element['data-langcode'],
+      ];
+    }
+
+    $form['attributes']['data-entity-uuid'] = [
       '#type' => 'value',
       '#title' => $entity_element['data-entity-uuid'],
-    );
-    $form['actions'] = array(
+    ];
+    $form['actions'] = [
       '#type' => 'actions',
-    );
+    ];
 
-    $form['actions']['save_modal'] = array(
+    $form['actions']['save_modal'] = [
       '#type' => 'submit',
       '#value' => $this->t('Next'),
       '#button_type' => 'primary',
       // No regular submit-handler. This form only works via JavaScript.
-      '#submit' => array(),
-      '#ajax' => array(
+      '#submit' => [],
+      '#ajax' => [
         'callback' => '::submitSelectStep',
         'event' => 'click',
-      ),
+      ],
       '#attributes' => [
         'class' => [
           'js-button-next',
         ],
       ],
-    );
+    ];
 
     return $form;
   }
@@ -298,47 +336,47 @@ public function buildSelectStep(array &$form, FormStateInterface $form_state) {
    * @return array
    *   The form structure.
    */
-  public function buildReviewStep(array &$form, FormStateInterface $form_state) {
+  public function buildReviewStep(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\Core\Entity\EntityInterface $entity */
     $entity = $form_state->get('entity');
 
-    $form['#title'] = $this->t('Review selected @type', array('@type' => $entity->getEntityType()->getLowercaseLabel()));
+    $form['#title'] = $this->t('Review selected @type', ['@type' => $entity->getEntityType()->getSingularLabel()]);
 
     $form['selection'] = [
       '#markup' => $entity->label(),
     ];
 
-    $form['actions'] = array(
+    $form['actions'] = [
       '#type' => 'actions',
-    );
+    ];
 
-    $form['actions']['back'] = array(
+    $form['actions']['back'] = [
       '#type' => 'submit',
       '#value' => $this->t('Replace selection'),
       // No regular submit-handler. This form only works via JavaScript.
-      '#submit' => array(),
-      '#ajax' => array(
+      '#submit' => [],
+      '#ajax' => [
         'callback' => '::submitAndShowSelect',
         'event' => 'click',
-      ),
-    );
+      ],
+    ];
 
-    $form['actions']['save_modal'] = array(
+    $form['actions']['save_modal'] = [
       '#type' => 'submit',
       '#value' => $this->t('Next'),
       '#button_type' => 'primary',
       // No regular submit-handler. This form only works via JavaScript.
-      '#submit' => array(),
-      '#ajax' => array(
+      '#submit' => [],
+      '#ajax' => [
         'callback' => '::submitAndShowEmbed',
         'event' => 'click',
-      ),
+      ],
       '#attributes' => [
         'class' => [
           'js-button-next',
         ],
       ],
-    );
+    ];
 
     return $form;
   }
@@ -355,7 +393,8 @@ public function buildReviewStep(array &$form, FormStateInterface $form_state) {
    *   The form structure.
    */
   public function buildEmbedStep(array $form, FormStateInterface $form_state) {
-    // Entity element is calculated on every AJAX request/submit. See ::buildForm().
+    // Entity element is calculated on every AJAX request/submit.
+    // See self::buildForm().
     $entity_element = $form_state->get('entity_element');
     /** @var \Drupal\embed\EmbedButtonInterface $embed_button */
     $embed_button = $form_state->get('embed_button');
@@ -365,31 +404,48 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) {
     $entity = $form_state->get('entity');
     $values = $form_state->getValues();
 
-    $form['#title'] = $this->t('Embed @type', array('@type' => $entity->getEntityType()->getLowercaseLabel()));
+    $form['#title'] = $this->t('Embed @type', ['@type' => $entity->getEntityType()->getSingularLabel()]);
 
-    $entity_label = '';
     try {
-      $entity_label = $entity->link();
+      if ($entity->getEntityType()->hasLinkTemplate('canonical')) {
+        $options = [
+          'attributes' => [
+            'target' => '_blank',
+          ],
+        ];
+        $entity_label = $entity->toLink($entity->label(), 'canonical', $options)->toString();
+      }
+      elseif ($entity->getEntityTypeId() == 'file') {
+        $entity_label = '<a href="' . file_create_url($entity->getFileUri()) . '" target="_blank">' . $entity->label() . '</a>';
+      }
+      else {
+        $entity_label = '<a href="' . $entity->toUrl()->toString() . '" target="_blank">' . $entity->label() . '</a>';
+      }
     }
     catch (\Exception $e) {
-      // Construct markup of the link to the entity manually if link() fails.
-      // @see https://www.drupal.org/node/2402533
-      $entity_label = '<a href="' . $entity->url() . '">' . $entity->label() . '</a>';
+      $entity_label = $entity->label();
     }
 
-    $form['entity'] = array(
+    $form['entity'] = [
       '#type' => 'item',
       '#title' => $this->t('Selected entity'),
       '#markup' => $entity_label,
-    );
-    $form['attributes']['data-entity-type'] = array(
+    ];
+    $form['attributes']['data-entity-type'] = [
       '#type' => 'hidden',
       '#value' => $entity_element['data-entity-type'],
-    );
-    $form['attributes']['data-entity-uuid'] = array(
+    ];
+    $form['attributes']['data-entity-uuid'] = [
       '#type' => 'hidden',
       '#value' => $entity_element['data-entity-uuid'],
-    );
+    ];
+
+    if (!empty($entity_element['data-langcode'])) {
+      $form['attributes']['data-langcode'] = [
+        '#type' => 'hidden',
+        '#value' => $entity_element['data-langcode'],
+      ];
+    }
 
     // Build the list of allowed Entity Embed Display plugins.
     $display_plugin_options = $this->getDisplayPluginOptions($embed_button, $entity);
@@ -407,55 +463,37 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) {
       $entity_element['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view';
     }
 
-    $form['attributes']['data-entity-embed-display'] = array(
+    $form['attributes']['data-entity-embed-display'] = [
       '#type' => 'select',
       '#title' => $this->t('Display as'),
       '#options' => $display_plugin_options,
       '#default_value' => $entity_element['data-entity-embed-display'],
       '#required' => TRUE,
-      '#ajax' => array(
+      '#ajax' => [
         'callback' => '::updatePluginConfigurationForm',
         'wrapper' => 'data-entity-embed-display-settings-wrapper',
         'effect' => 'fade',
-      ),
+      ],
       // Hide the selection if only one option is available.
       '#access' => count($display_plugin_options) > 1,
-    );
-    $form['attributes']['data-entity-embed-display-settings'] = array(
+    ];
+    $form['attributes']['data-entity-embed-display-settings'] = [
       '#type' => 'container',
       '#prefix' => '<div id="data-entity-embed-display-settings-wrapper">',
       '#suffix' => '</div>',
-    );
-    $form['attributes']['data-embed-button'] = array(
+    ];
+    $form['attributes']['data-embed-button'] = [
       '#type' => 'value',
       '#value' => $embed_button->id(),
-    );
+    ];
     $plugin_id = !empty($values['attributes']['data-entity-embed-display']) ? $values['attributes']['data-entity-embed-display'] : $entity_element['data-entity-embed-display'];
     if (!empty($plugin_id)) {
-      if (is_string($entity_element['data-entity-embed-display-settings'])) {
-        $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']);
+      if (empty($entity_element['data-entity-embed-display-settings'])) {
+        $entity_element['data-entity-embed-display-settings'] = [];
       }
-
-      // Supress Drupal's "Link image to" dropdown when embedding an image,
-      // since the 'Link to' option provides this functionality.
-      if (isset($form['attributes']['data-entity-embed-display-settings']['image_link'])) {
-        $form['attributes']['data-entity-embed-display-settings']['image_link']['#type'] = 'hidden';
-        $form['attributes']['data-entity-embed-display-settings']['image_link']['#value'] = '';
+      elseif (is_string($entity_element['data-entity-embed-display-settings'])) {
+        $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']);
       }
-      $form['attributes']['data-entity-embed-display-settings']['link_url'] = [
-        '#title' => t('Link to'),
-        '#type' => 'entity_autocomplete',
-        '#target_type' => 'node',
-        '#attributes' => [
-          'data-autocomplete-first-character-blacklist' => '/#?'
-        ],
-        '#element_validate' => [[get_called_class(), 'validateUriElement']],
-        '#process_default_value' => FALSE,
-        '#description' => $this->t('Start typing the title of a piece of content to select it. You can also enter an internal path such as %add-node or an external URL such as %url. Enter %front to link to the front page.', ['%front' => '<front>', '%add-node' => '/node/add', '%url' => 'http://example.com']),
-        '#default_value' => isset($entity_element['data-entity-embed-display-settings']['link_url']) ? $this->getUriAsDisplayableString($entity_element['data-entity-embed-display-settings']['link_url']) : '',
-        '#maxlength' => 2048,
-      ];
-
       $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $entity_element['data-entity-embed-display-settings']);
       $display->setContextValue('entity', $entity);
       $display->setAttributes($entity_element);
@@ -465,175 +503,64 @@ public function buildEmbedStep(array $form, FormStateInterface $form_state) {
     // When Drupal core's filter_align is being used, the text editor may
     // offer the ability to change the alignment.
     if ($editor->getFilterFormat()->filters('filter_align')->status) {
-      $form['attributes']['data-align'] = array(
+      $form['attributes']['data-align'] = [
         '#title' => $this->t('Align'),
         '#type' => 'radios',
-        '#options' => array(
+        '#options' => [
           '' => $this->t('None'),
           'left' => $this->t('Left'),
           'center' => $this->t('Center'),
           'right' => $this->t('Right'),
-        ),
+        ],
         '#default_value' => isset($entity_element['data-align']) ? $entity_element['data-align'] : '',
-        '#wrapper_attributes' => array('class' => array('container-inline')),
-        '#attributes' => array('class' => array('container-inline')),
-      );
+        '#wrapper_attributes' => ['class' => ['container-inline']],
+        '#attributes' => ['class' => ['container-inline']],
+      ];
     }
 
     // When Drupal core's filter_caption is being used, the text editor may
     // offer the ability to add a caption.
     if ($editor->getFilterFormat()->filters('filter_caption')->status) {
-      $form['attributes']['data-caption'] = array(
+      $form['attributes']['data-caption'] = [
         '#title' => $this->t('Caption'),
-        '#type' => 'textfield',
+        '#type' => 'textarea',
+        '#rows' => 3,
         '#default_value' => isset($entity_element['data-caption']) ? Html::decodeEntities($entity_element['data-caption']) : '',
-        '#element_validate' => array('::escapeValue'),
-      );
+        '#element_validate' => ['::escapeValue'],
+      ];
     }
 
-    $form['actions'] = array(
+    $form['actions'] = [
       '#type' => 'actions',
-    );
-    $form['actions']['back'] = array(
+    ];
+    $form['actions']['back'] = [
       '#type' => 'submit',
       '#value' => $this->t('Back'),
       // No regular submit-handler. This form only works via JavaScript.
-      '#submit' => array(),
-      '#ajax' => array(
+      '#submit' => [],
+      '#ajax' => [
         'callback' => !empty($this->entityBrowserSettings['display_review']) ? '::submitAndShowReview' : '::submitAndShowSelect',
         'event' => 'click',
-      ),
-    );
-    $form['actions']['save_modal'] = array(
+      ],
+    ];
+    $form['actions']['save_modal'] = [
       '#type' => 'submit',
       '#value' => $this->t('Embed'),
       '#button_type' => 'primary',
       // No regular submit-handler. This form only works via JavaScript.
-      '#submit' => array(),
-      '#ajax' => array(
+      '#submit' => [],
+      '#ajax' => [
         'callback' => '::submitEmbedStep',
         'event' => 'click',
-      ),
-    );
+      ],
+    ];
 
     return $form;
   }
 
   /**
-   * Gets the URI without the 'internal:' or 'entity:' scheme.
-   *
-   * The following two forms of URIs are transformed:
-   * - 'entity:' URIs: to entity autocomplete ("label (entity id)") strings;
-   * - 'internal:' URIs: the scheme is stripped.
-   *
-   * This method is the inverse of ::getUserEnteredStringAsUri().
-   *
-   * @param string $uri
-   *   The URI to get the displayable string for.
-   *
-   * @return string
-   *
-   * @see static::getUserEnteredStringAsUri()
-   */
-  protected function getUriAsDisplayableString($uri) {
-    $uri = Html::decodeEntities($uri);
-    $scheme = parse_url($uri, PHP_URL_SCHEME);
-
-    // By default, the displayable string is the URI.
-    $displayable_string = $uri;
-
-    // A different displayable string may be chosen in case of the 'internal:'
-    // or 'entity:' built-in schemes.
-    if ($scheme === 'internal') {
-      $uri_reference = explode(':', $uri, 2)[1];
-
-      // @todo '<front>' is valid input for BC reasons, may be removed by
-      //   https://www.drupal.org/node/2421941
-      $path = parse_url($uri, PHP_URL_PATH);
-      if ($path === '/') {
-        $uri_reference = '<front>' . substr($uri_reference, 1);
-      }
-
-      $displayable_string = $uri_reference;
-    }
-    elseif ($scheme === 'entity') {
-      list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2);
-      // Show the 'entity:' URI as the entity autocomplete would.
-      // @todo Support entity types other than 'node'. Will be fixed in
-      //    https://www.drupal.org/node/2423093.
-      if ($entity_type == 'node' && $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) {
-        $displayable_string = EntityAutocomplete::getEntityLabels([$entity]);
-      }
-    }
-
-    return $displayable_string;
-  }
-
-  /**
-   * Gets the user-entered string as a URI.
-   *
-   * The following two forms of input are mapped to URIs:
-   * - entity autocomplete ("label (entity id)") strings: to 'entity:' URIs;
-   * - strings without a detectable scheme: to 'internal:' URIs.
-   *
-   * This method is the inverse of ::getUriAsDisplayableString().
-   *
-   * @param string $string
-   *   The user-entered string.
-   *
-   * @return string
-   *   The URI, if a non-empty $uri was passed.
-   *
-   * @see static::getUriAsDisplayableString()
-   */
-  protected static function getUserEnteredStringAsUri($string) {
-    // By default, assume the entered string is an URI.
-    $uri = $string;
-
-    // Detect entity autocomplete string, map to 'entity:' URI.
-    $entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($string);
-    if ($entity_id !== NULL) {
-      // @todo Support entity types other than 'node'. Will be fixed in
-      //    https://www.drupal.org/node/2423093.
-      $uri = 'entity:node/' . $entity_id;
-    }
-    // Detect a schemeless string, map to 'internal:' URI.
-    elseif (!empty($string) && parse_url($string, PHP_URL_SCHEME) === NULL) {
-      // @todo '<front>' is valid input for BC reasons, may be removed by
-      //   https://www.drupal.org/node/2421941
-      // - '<front>' -> '/'
-      // - '<front>#foo' -> '/#foo'
-      if (strpos($string, '<front>') === 0) {
-        $string = '/' . substr($string, strlen('<front>'));
-      }
-      $uri = 'internal:' . $string;
-    }
-
-    return $uri;
-  }
-
-  /**
-   * Form element validation handler for the 'uri' element.
-   *
-   * Disallows saving inaccessible or untrusted URLs.
+   * {@inheritdoc}
    */
-  public static function validateUriElement($element, FormStateInterface $form_state, $form) {
-    $uri = static::getUserEnteredStringAsUri($element['#value']);
-    $form_state->setValueForElement($element, $uri);
-
-    // If getUserEnteredStringAsUri() mapped the entered value to a 'internal:'
-    // URI , ensure the raw value begins with '/', '?' or '#'.
-    // @todo '<front>' is valid input for BC reasons, may be removed by
-    //   https://www.drupal.org/node/2421941
-    if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') {
-      $form_state->setError($element, t('Manually entered paths should start with /, ? or #.'));
-      return;
-    }
-  }
-
-   /**
-    * {@inheritdoc}
-    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
     parent::validateForm($form, $form_state);
 
@@ -655,7 +582,9 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    */
   public function validateSelectStep(array $form, FormStateInterface $form_state) {
     if ($form_state->hasValue(['entity_browser', 'entities'])) {
-      $id = $form_state->getValue(['entity_browser', 'entities', 0])->id();
+      if (count($form_state->getValue(['entity_browser', 'entities'])) > 0) {
+        $id = $form_state->getValue(['entity_browser', 'entities', 0])->id();
+      }
       $element = $form['entity_browser'];
     }
     else {
@@ -665,16 +594,20 @@ public function validateSelectStep(array $form, FormStateInterface $form_state)
 
     $entity_type = $form_state->getValue(['attributes', 'data-entity-type']);
 
+    if (!isset($id)) {
+      $form_state->setError($element, $this->t('No entity selected.'));
+      return;
+    }
     if ($entity = $this->entityTypeManager->getStorage($entity_type)->load($id)) {
       if (!$entity->access('view')) {
-        $form_state->setError($element, $this->t('Unable to access @type entity @id.', array('@type' => $entity_type, '@id' => $id)));
+        $form_state->setError($element, $this->t('Unable to access @type entity @id.', ['@type' => $entity_type, '@id' => $id]));
       }
       else {
         if ($uuid = $entity->uuid()) {
           $form_state->setValueForElement($form['attributes']['data-entity-uuid'], $uuid);
         }
         else {
-          $form_state->setError($element, $this->t('Cannot embed @type entity @id because it does not have a UUID.', array('@type' => $entity_type, '@id' => $id)));
+          $form_state->setError($element, $this->t('Cannot embed @type entity @id because it does not have a UUID.', ['@type' => $entity_type, '@id' => $id]));
         }
 
         // Ensure that at least one Entity Embed Display plugin is present
@@ -684,13 +617,17 @@ public function validateSelectStep(array $form, FormStateInterface $form_state)
         // If no plugin is available after taking the intersection, raise error.
         // Also log an exception.
         if (empty($display_plugin_options)) {
-          $form_state->setError($element, $this->t('No display options available for the selected entity. Please select another entity.'));
-          $this->logger('entity_embed')->warning('No display options available for "@type:" entity "@id" while embedding using button "@button". Please ensure that at least one Entity Embed Display plugin is allowed for this embed button which is available for this entity.', array('@type' => $entity_type, '@id' => $entity->id(), '@button' => $embed_button->id()));
+          $form_state->setError($element, $this->t('No display options available for the selected %entity-type. Please select another %entity_type.', ['%entity_type' => $entity->getEntityType()->getLabel()]));
+          $this->logger('entity_embed')->warning('No display options available for "@type:" entity "@id" while embedding using button "@button". Please ensure that at least one Entity Embed Display plugin is allowed for this embed button which is available for this entity.', [
+            '@type' => $entity_type,
+            '@id' => $entity->id(),
+            '@button' => $embed_button->id(),
+          ]);
         }
       }
     }
     else {
-      $form_state->setError($element, $this->t('Unable to load @type entity @id.', array('@type' => $entity_type, '@id' => $id)));
+      $form_state->setError($element, $this->t('Unable to load @type entity @id.', ['@type' => $entity_type, '@id' => $id]));
     }
   }
 
@@ -705,11 +642,9 @@ public function validateSelectStep(array $form, FormStateInterface $form_state)
   public function validateEmbedStep(array $form, FormStateInterface $form_state) {
     // Validate configuration forms for the Entity Embed Display plugin used.
     $entity_element = $form_state->getValue('attributes');
-    $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type'])
-      ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]);
-    $entity = current($entity) ?: NULL;
+    $entity = $form_state->get('entity');
     $plugin_id = $entity_element['data-entity-embed-display'];
-    $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : array();
+    $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : [];
     $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $plugin_settings);
     $display->setContextValue('entity', $entity);
     $display->setAttributes($entity_element);
@@ -740,6 +675,8 @@ public function updatePluginConfigurationForm(array &$form, FormStateInterface $
    *   The form array.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The form state.
+   * @param string $step
+   *   The next step name, such as 'select', 'review' or 'embed'.
    *
    * @return \Drupal\Core\Ajax\AjaxResponse
    *   The ajax response.
@@ -778,10 +715,10 @@ public function submitSelectStep(array &$form, FormStateInterface $form_state) {
     // Display errors in form, if any.
     if ($form_state->hasAnyErrors()) {
       unset($form['#prefix'], $form['#suffix']);
-      $form['status_messages'] = array(
+      $form['status_messages'] = [
         '#type' => 'status_messages',
         '#weight' => -10,
-      );
+      ];
       $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $form));
     }
     else {
@@ -851,8 +788,8 @@ public function submitAndShowEmbed(array $form, FormStateInterface $form_state)
    *
    * @param array $form
    *   An associative array containing the structure of the form.
-   * @param FormStateInterface $form_state
-   *   An associative array containing the current state of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
    *
    * @return \Drupal\Core\Ajax\AjaxResponse
    *   The ajax response.
@@ -862,11 +799,9 @@ public function submitEmbedStep(array &$form, FormStateInterface $form_state) {
 
     // Submit configuration form the selected Entity Embed Display plugin.
     $entity_element = $form_state->getValue('attributes');
-    $entity = $this->entityTypeManager->getStorage($entity_element['data-entity-type'])
-      ->loadByProperties(['uuid' => $entity_element['data-entity-uuid']]);
-    $entity = current($entity);
+    $entity = $this->loadEntityByAttributes($entity_element);
     $plugin_id = $entity_element['data-entity-embed-display'];
-    $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : array();
+    $plugin_settings = !empty($entity_element['data-entity-embed-display-settings']) ? $entity_element['data-entity-embed-display-settings'] : [];
     $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $plugin_settings);
     $display->setContextValue('entity', $entity);
     $display->setAttributes($entity_element);
@@ -876,10 +811,10 @@ public function submitEmbedStep(array &$form, FormStateInterface $form_state) {
     // Display errors in form, if any.
     if ($form_state->hasAnyErrors()) {
       unset($form['#prefix'], $form['#suffix']);
-      $form['status_messages'] = array(
+      $form['status_messages'] = [
         '#type' => 'status_messages',
         '#weight' => -10,
-      );
+      ];
       $response->addCommand(new HtmlCommand('#entity-embed-dialog-form', $form));
     }
     else {
@@ -889,12 +824,13 @@ public function submitEmbedStep(array &$form, FormStateInterface $form_state) {
       }
 
       // Filter out empty attributes.
-      $values['attributes'] = array_filter($values['attributes'], function($value) {
-        return (bool) Unicode::strlen((string) $value);
+      $values['attributes'] = array_filter($values['attributes'], function ($value) {
+        return (bool) mb_strlen((string) $value);
       });
 
-      // Allow other modules to alter the values before getting submitted to the WYSIWYG.
-      $this->moduleHandler->alter('entity_embed_values', $values, $entity, $display, $form_state);
+      // Allow other modules to alter the values before getting submitted to the
+      // WYSIWYG.
+      $this->moduleHandler->alter('entity_embed_values', $values, $entity, $display);
 
       $response->addCommand(new EditorDialogSave($values));
       $response->addCommand(new CloseModalDialogCommand());
@@ -916,8 +852,7 @@ public static function escapeValue($element, FormStateInterface $form_state) {
   }
 
   /**
-   * Returns the allowed Entity Embed Display plugins given an embed button and
-   * an entity.
+   * Returns the allowed display plugins given an embed button and an entity.
    *
    * @param \Drupal\embed\EmbedButtonInterface $embed_button
    *   The embed button.
@@ -928,19 +863,20 @@ public static function escapeValue($element, FormStateInterface $form_state) {
    *   List of allowed Entity Embed Display plugins.
    */
   public function getDisplayPluginOptions(EmbedButtonInterface $embed_button, EntityInterface $entity) {
-    $plugins = $this->entityEmbedDisplayManager->getDefinitionOptionsForEntity($entity);
+    $plugins = $this->entityEmbedDisplayManager->getDefinitionOptionsForContext([
+      'entity' => $entity,
+      'entity_type' => $entity->getEntityTypeId(),
+      'embed_button' => $embed_button,
+    ]);
 
-    if ($allowed_plugins = $embed_button->getTypeSetting('display_plugins')) {
-      $plugins = array_intersect_key($plugins, array_flip($allowed_plugins));
-    }
-
-    natsort($plugins);
     return $plugins;
   }
 
   /**
-   * Registers JS callback that gets entities from entity browser and updates
-   * form values accordingly.
+   * Registers JS callbacks.
+   *
+   * Callbacks are responsible for getting entities from entity browser and
+   * updating form values accordingly.
    */
   public function registerJSCallback(RegisterJSCallbacks $event) {
     if ($event->getBrowserID() == $this->entityBrowser->id()) {
@@ -952,6 +888,7 @@ public function registerJSCallback(RegisterJSCallbacks $event) {
    * Load the current entity browser and its settings from the form state.
    *
    * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
    */
   protected function loadEntityBrowser(FormStateInterface $form_state) {
     $this->entityBrowser = NULL;
diff --git a/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php b/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php
index 2a6fa14b95..6e71eaadba 100644
--- a/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php
+++ b/web/modules/entity_embed/src/Plugin/CKEditorPlugin/DrupalEntity.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\entity_embed\Plugin\CKEditorPlugin;
 
+use Drupal\ckeditor\CKEditorPluginCssInterface;
 use Drupal\editor\Entity\Editor;
 use Drupal\embed\EmbedButtonInterface;
 use Drupal\embed\EmbedCKEditorPluginBase;
@@ -15,7 +16,7 @@
  *   embed_type_id = "entity"
  * )
  */
-class DrupalEntity extends EmbedCKEditorPluginBase {
+class DrupalEntity extends EmbedCKEditorPluginBase implements CKEditorPluginCssInterface {
 
   /**
    * {@inheritdoc}
@@ -33,15 +34,36 @@ public function getFile() {
     return drupal_get_path('module', 'entity_embed') . '/js/plugins/drupalentity/plugin.js';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getLibraries(Editor $editor) {
+    return [
+      'core/jquery',
+      'core/drupal',
+      'core/drupal.ajax',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
   public function getConfig(Editor $editor) {
-    return array(
+    return [
       'DrupalEntity_dialogTitleAdd' => t('Insert entity'),
       'DrupalEntity_dialogTitleEdit' => t('Edit entity'),
       'DrupalEntity_buttons' => $this->getButtons(),
-    );
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCssFiles(Editor $editor) {
+    return [
+      drupal_get_path('module', 'system') . '/css/components/hidden.module.css',
+      drupal_get_path('module', 'entity_embed') . '/css/entity_embed.editor.css',
+    ];
   }
 
 }
diff --git a/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php b/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php
index 22740687ed..d420acccf2 100644
--- a/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php
+++ b/web/modules/entity_embed/src/Plugin/Derivative/FieldFormatterDeriver.php
@@ -18,7 +18,7 @@ class FieldFormatterDeriver extends DeriverBase implements ContainerDeriverInter
   /**
    * The manager for formatter plugins.
    *
-   * @var \Drupal\Core\Field\FormatterPluginManager.
+   * @var \Drupal\Core\Field\FormatterPluginManager
    */
   protected $formatterManager;
 
@@ -65,16 +65,21 @@ public function getDerivativeDefinitions($base_plugin_definition) {
     if (!isset($base_plugin_definition['field_type'])) {
       throw new \LogicException("Undefined field_type definition in plugin {$base_plugin_definition['id']}.");
     }
-    $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode');
+
+    $no_media_image_decorator = [
+      'entity_reference_entity_id',
+      'entity_reference_label',
+    ];
+
     foreach ($this->formatterManager->getOptions($base_plugin_definition['field_type']) as $formatter => $label) {
       $this->derivatives[$formatter] = $base_plugin_definition;
       $this->derivatives[$formatter]['label'] = $label;
-      // Don't show entity_reference_entity_view in the UI if the rendered
-      // entity mode is FALSE. In that case we show view modes from
-      // ViewModeDeriver, entity_reference_entity_view is kept for backwards
-      // compatibility.
-      if ($formatter == 'entity_reference_entity_view' && $mode == FALSE) {
-        $this->derivatives[$formatter]['no_ui'] = TRUE;
+
+      // The base entity embed display plugin annotation has opted into
+      // `supports_image_alt_and_title`. For some derivatives we know that they
+      // do not support this, so opt them back out.
+      if (in_array($formatter, $no_media_image_decorator, TRUE)) {
+        $this->derivatives[$formatter]['supports_image_alt_and_title'] = FALSE;
       }
     }
     return $this->derivatives;
diff --git a/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php b/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php
index f23d70e032..75fc762c3e 100644
--- a/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php
+++ b/web/modules/entity_embed/src/Plugin/Derivative/ViewModeDeriver.php
@@ -56,14 +56,18 @@ public static function create(ContainerInterface $container, $base_plugin_id) {
    * {@inheritdoc}
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
-    $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode');
+    $no_ui = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode');
     foreach ($this->entityDisplayRepository->getAllViewModes() as $view_modes) {
       foreach ($view_modes as $view_mode => $definition) {
-        $this->derivatives[$definition['id']] = $base_plugin_definition;
-        $this->derivatives[$definition['id']]['label'] = $definition['label'];
-        $this->derivatives[$definition['id']]['view_mode'] = $view_mode;
-        $this->derivatives[$definition['id']]['entity_types'] = $definition['targetEntityType'];
-        $this->derivatives[$definition['id']]['no_ui'] = $mode;
+        $this->derivatives[$definition['id']] = [
+          'label' => $definition['label'],
+          'view_mode' => $view_mode,
+          'entity_types' => [$definition['targetEntityType']],
+          'no_ui' => $no_ui,
+          // Check if the plugin should run through MediaImageDecorator. A more
+          // fine-grained access check happens there.
+          'supports_image_alt_and_title' => ($definition['targetEntityType'] === 'media'),
+        ] + $base_plugin_definition;
       }
     }
     return $this->derivatives;
diff --git a/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php b/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php
index 967ffb3a13..554ff3d502 100644
--- a/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php
+++ b/web/modules/entity_embed/src/Plugin/EmbedType/Entity.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Plugin\PluginDependencyTrait;
 use Drupal\embed\EmbedType\EmbedTypeBase;
+use Drupal\entity_browser\EntityBrowserInterface;
 use Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -102,7 +103,9 @@ public function defaultConfiguration() {
       'bundles' => [],
       'display_plugins' => [],
       'entity_browser' => '',
-      'entity_browser_settings' => [],
+      'entity_browser_settings' => [
+        'display_review' => 0,
+      ],
     ];
   }
 
@@ -113,56 +116,59 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     $embed_button = $form_state->getTemporaryValue('embed_button');
     $entity_type_id = $this->getConfigurationValue('entity_type');
 
-    $form['entity_type'] = array(
+    $form['entity_type'] = [
       '#type' => 'select',
       '#title' => $this->t('Entity type'),
       '#options' => $this->getEntityTypeOptions(),
       '#default_value' => $entity_type_id,
-      '#description' => $this->t("Entity type for which this button is to enabled."),
+      '#description' => $this->t("The entity type this button will embed."),
       '#required' => TRUE,
-      '#ajax' => array(
-        'callback' => array($form_state->getFormObject(), 'updateTypeSettings'),
+      '#ajax' => [
+        'callback' => [$form_state->getFormObject(), 'updateTypeSettings'],
         'effect' => 'fade',
-      ),
+      ],
       '#disabled' => !$embed_button->isNew(),
-    );
+    ];
 
     if ($entity_type_id) {
       $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
-      $form['bundles'] = array(
+      $form['bundles'] = [
         '#type' => 'checkboxes',
         '#title' => $entity_type->getBundleLabel() ?: $this->t('Bundles'),
         '#options' => $this->getEntityBundleOptions($entity_type),
         '#default_value' => $this->getConfigurationValue('bundles'),
         '#description' => $this->t('If none are selected, all are allowed.'),
-      );
+      ];
       $form['bundles']['#access'] = !empty($form['bundles']['#options']);
 
       // Allow option to limit Entity Embed Display plugins.
-      $form['display_plugins'] = array(
+      $form['display_plugins'] = [
         '#type' => 'checkboxes',
         '#title' => $this->t('Allowed Entity Embed Display plugins'),
         '#options' => $this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id),
         '#default_value' => $this->getConfigurationValue('display_plugins'),
         '#description' => $this->t('If none are selected, all are allowed. Note that these are the plugins which are allowed for this entity type, all of these might not be available for the selected entity.'),
-      );
+      ];
       $form['display_plugins']['#access'] = !empty($form['display_plugins']['#options']);
 
       /** @var \Drupal\entity_browser\EntityBrowserInterface[] $browsers */
       if ($this->entityTypeManager->hasDefinition('entity_browser') && ($browsers = $this->entityTypeManager->getStorage('entity_browser')->loadMultiple())) {
-        $ids = array_keys($browsers);
-        $labels = array_map(
+        // Filter out unsupported displays & return array of ids and labels.
+        $browsers = array_map(
           function ($item) {
             /** @var \Drupal\entity_browser\EntityBrowserInterface $item */
             return $item->label();
           },
-          $browsers
+          // Filter out both modal and standalone forms as they don't work.
+          array_filter($browsers, function (EntityBrowserInterface $browser) {
+            return !in_array($browser->getDisplay()->getPluginId(), ['modal', 'standalone'], TRUE);
+          })
         );
-        $options = ['_none' => $this->t('None (autocomplete)')] + array_combine($ids, $labels);
+        $options = ['_none' => $this->t('None (autocomplete)')] + $browsers;
         $form['entity_browser'] = [
           '#type' => 'select',
           '#title' => $this->t('Entity browser'),
-          '#description' => $this->t('Entity browser to be used to select entities to be embedded.'),
+          '#description' => $this->t('Entity browser to be used to select entities to be embedded. Only compatible browsers will be available to be chosen.'),
           '#options' => $options,
           '#default_value' => $this->getConfigurationValue('entity_browser'),
         ];
@@ -204,7 +210,7 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
     $form_state->setValue('display_plugins', array_keys(array_filter($display_plugins)));
     $entity_browser = $form_state->getValue('entity_browser') == '_none' ? '' : $form_state->getValue('entity_browser');
     $form_state->setValue('entity_browser', $entity_browser);
-    $form_state->setValue('entity_browser_settings', $form_state->getValue('entity_browser_settings'));
+    $form_state->setValue('entity_browser_settings', $form_state->getValue('entity_browser_settings', []));
 
     parent::submitConfigurationForm($form, $form_state);
   }
@@ -228,12 +234,12 @@ protected function getEntityTypeOptions() {
           unset($options[$group][$entity_type_id]);
         }
         // Filter out entity types that do not support UUIDs.
-        if (!$this->entityTypeManager->getDefinition($entity_type_id)->hasKey('uuid')) {
+        elseif (!$this->entityTypeManager->getDefinition($entity_type_id)->hasKey('uuid')) {
           unset($options[$group][$entity_type_id]);
         }
         // Filter out entity types that will not have any Entity Embed Display
         // plugins.
-        if (!$this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id)) {
+        elseif (!$this->displayPluginManager->getDefinitionOptionsForEntityType($entity_type_id)) {
           unset($options[$group][$entity_type_id]);
         }
       }
@@ -252,7 +258,7 @@ protected function getEntityTypeOptions() {
    *   An array of bundle labels, keyed by bundle name.
    */
   protected function getEntityBundleOptions(EntityTypeInterface $entity_type) {
-    $bundle_options = array();
+    $bundle_options = [];
     // If the entity has bundles, allow option to restrict to bundle(s).
     if ($entity_type->hasKey('bundle')) {
       foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type->id()) as $bundle_id => $bundle_info) {
@@ -292,6 +298,16 @@ public function calculateDependencies() {
       $this->calculatePluginDependencies($instance);
     }
 
+    $entity_browser = $this->getConfigurationValue('entity_browser');
+    if ($entity_browser && $this->entityTypeManager->hasDefinition('entity_browser')) {
+      $browser = $this->entityTypeManager
+        ->getStorage('entity_browser')
+        ->load($entity_browser);
+      if ($browser) {
+        $this->addDependency($browser->getConfigDependencyKey(), $browser->getConfigDependencyName());
+      }
+    }
+
     return $this->dependencies;
   }
 
diff --git a/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php b/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php
index b230b570c2..f244f89686 100644
--- a/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php
+++ b/web/modules/entity_embed/src/Plugin/Filter/EntityEmbedFilter.php
@@ -3,14 +3,15 @@
 namespace Drupal\entity_embed\Plugin\Filter;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\entity_embed\EntityEmbedBuilderInterface;
 use Drupal\entity_embed\Exception\EntityNotFoundException;
-use Drupal\entity_embed\Exception\RecursiveRenderingException;
 use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -22,14 +23,22 @@
  * @Filter(
  *   id = "entity_embed",
  *   title = @Translation("Display embedded entities"),
- *   description = @Translation("Embeds entities using data attributes: data-entity-type, data-entity-uuid, and data-view-mode."),
- *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ *   description = @Translation("Embeds entities using data attributes: data-entity-type, data-entity-uuid, and data-view-mode. Should usually run as the last filter, since it does not contain user input."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE,
+ *   weight = 100,
  * )
  */
 class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInterface {
 
   use DomHelperTrait;
 
+  /**
+   * The number of times this formatter allows rendering the same entity.
+   *
+   * @var int
+   */
+  const RECURSIVE_RENDER_LIMIT = 20;
+
   /**
    * The renderer service.
    *
@@ -51,6 +60,25 @@ class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInte
    */
   protected $builder;
 
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * An array of counters for the recursive rendering protection.
+   *
+   * Each counter takes into account all the relevant information about the
+   * field and the referenced entity that is being rendered.
+   *
+   * @var array
+   *
+   * @see \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter::$recursiveRenderDepth
+   */
+  protected static $recursiveRenderDepth = [];
+
   /**
    * Constructs a EntityEmbedFilter object.
    *
@@ -66,12 +94,15 @@ class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInte
    *   The renderer.
    * @param \Drupal\entity_embed\EntityEmbedBuilderInterface $builder
    *   The entity embed builder service.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, EntityEmbedBuilderInterface $builder) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, EntityEmbedBuilderInterface $builder, LoggerChannelFactoryInterface $logger_factory) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->entityTypeManager = $entity_type_manager;
     $this->renderer = $renderer;
     $this->builder = $builder;
+    $this->loggerFactory = $logger_factory;
   }
 
   /**
@@ -84,7 +115,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_definition,
       $container->get('entity_type.manager'),
       $container->get('renderer'),
-      $container->get('entity_embed.builder')
+      $container->get('entity_embed.builder'),
+      $container->get('logger.factory')
     );
   }
 
@@ -111,10 +143,10 @@ public function process($text, $langcode) {
           $node->removeAttribute('data-entity-embed-settings');
         }
 
+        $entity = NULL;
         try {
           // Load the entity either by UUID (preferred) or ID.
           $id = NULL;
-          $entity = NULL;
           if ($id = $node->getAttribute('data-entity-uuid')) {
             $entity = $this->entityTypeManager->getStorage($entity_type)
               ->loadByProperties(['uuid' => $id]);
@@ -124,22 +156,48 @@ public function process($text, $langcode) {
             $id = $node->getAttribute('data-entity-id');
             $entity = $this->entityTypeManager->getStorage($entity_type)->load($id);
           }
+          if (!$entity instanceof EntityInterface) {
+            $missing_text = $this->t('Missing @type.', ['@type' => $this->entityTypeManager->getDefinition($entity_type)->getSingularLabel()]);
+            $entity_output = '<img src="' . file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')) . '" width="180" height="180" alt="' . $missing_text . '" title="' . $missing_text . '"/>';
+            throw new EntityNotFoundException(sprintf('Unable to load embedded %s entity %s.', $entity_type, $id));
+          }
+        }
+        catch (EntityNotFoundException $e) {
+          watchdog_exception('entity_embed', $e);
+        }
+
+        if ($entity instanceof EntityInterface) {
+          // If a UUID was not used, but is available, add it to the HTML.
+          if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) {
+            $node->setAttribute('data-entity-uuid', $uuid);
+          }
+
+          $context = $this->getNodeAttributesAsArray($node);
+          $context += ['data-langcode' => $langcode];
+
+          // Due to render caching and delayed calls, filtering happens later
+          // in the rendering process through a '#pre_render' callback, so we
+          // need to generate a counter that takes into account all the
+          // relevant information about this field and the referenced entity
+          // that is being rendered.
+          // @see \Drupal\filter\Element\ProcessedText::preRenderText()
+          $recursive_render_id = $entity->uuid() . json_encode($context);
+          if (isset(static::$recursiveRenderDepth[$recursive_render_id])) {
+            static::$recursiveRenderDepth[$recursive_render_id]++;
+          }
+          else {
+            static::$recursiveRenderDepth[$recursive_render_id] = 1;
+          }
 
-          if ($entity) {
-            // Protect ourselves from recursive rendering.
-            static $depth = 0;
-            $depth++;
-            if ($depth > 20) {
-              throw new RecursiveRenderingException(sprintf('Recursive rendering detected when rendering embedded %s entity %s.', $entity_type, $entity->id()));
-            }
-
-            // If a UUID was not used, but is available, add it to the HTML.
-            if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) {
-              $node->setAttribute('data-entity-uuid', $uuid);
-            }
-
-            $context = $this->getNodeAttributesAsArray($node);
-            $context += array('data-langcode' => $langcode);
+          // Protect ourselves from recursive rendering.
+          if (static::$recursiveRenderDepth[$recursive_render_id] > static::RECURSIVE_RENDER_LIMIT) {
+            $this->loggerFactory->get('entity')->error('Recursive rendering detected when rendering embedded entity %entity_type: %entity_id. Aborting rendering.', [
+              '%entity_type' => $entity->getEntityTypeId(),
+              '%entity_id' => $entity->id(),
+            ]);
+            $entity_output = '';
+          }
+          else {
             $build = $this->builder->buildEntityEmbed($entity, $context);
             // We need to render the embedded entity:
             // - without replacing placeholders, so that the placeholders are
@@ -154,16 +212,8 @@ public function process($text, $langcode) {
               return $this->renderer->render($build);
             });
             $result = $result->merge(BubbleableMetadata::createFromRenderArray($build));
-
-            $depth--;
-          }
-          else {
-            throw new EntityNotFoundException(sprintf('Unable to load embedded %s entity %s.', $entity_type, $id));
           }
         }
-        catch (\Exception $e) {
-          watchdog_exception('entity_embed', $e);
-        }
 
         $this->replaceNodeContent($node, $entity_output);
       }
diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php
index 318635a51b..b9e3d3ddc9 100644
--- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php
+++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php
@@ -2,7 +2,16 @@
 
 namespace Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FormatterPluginManager;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Security\TrustedCallbackInterface;
+use Drupal\Core\TypedData\TypedDataManager;
 use Drupal\entity_embed\EntityEmbedDisplay\FieldFormatterEntityEmbedDisplayBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Entity Embed Display reusing entity reference field formatters.
@@ -13,10 +22,70 @@
  *   id = "entity_reference",
  *   label = @Translation("Entity Reference"),
  *   deriver = "Drupal\entity_embed\Plugin\Derivative\FieldFormatterDeriver",
- *   field_type = "entity_reference"
+ *   field_type = "entity_reference",
+ *   supports_image_alt_and_title = TRUE
  * )
  */
-class EntityReferenceFieldFormatter extends FieldFormatterEntityEmbedDisplayBase {
+class EntityReferenceFieldFormatter extends FieldFormatterEntityEmbedDisplayBase implements TrustedCallbackInterface {
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a new EntityReferenceFieldFormatter.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager
+   *   The field formatter plugin manager.
+   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
+   *   The typed data manager.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface|null $config_factory
+   *   The configuration factory, or null to get from global container for
+   *   backwards compatibility.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, TypedDataManager $typed_data_manager, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory = NULL) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $formatter_plugin_manager, $typed_data_manager, $language_manager);
+    $this->configFactory = $config_factory instanceof ConfigFactoryInterface ? $config_factory : \Drupal::configFactory();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.field.formatter'),
+      $container->get('typed_data_manager'),
+      $container->get('language_manager'),
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks() {
+    return [
+      'disableContextualLinks',
+      'disableQuickEdit',
+    ];
+  }
 
   /**
    * {@inheritdoc}
@@ -33,7 +102,107 @@ public function getFieldDefinition() {
    * {@inheritdoc}
    */
   public function getFieldValue() {
-    return array('target_id' => $this->getContextValue('entity')->id());
+    return ['target_id' => $this->getContextValue('entity')->id()];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function isApplicableFieldFormatter() {
+    $access = parent::isApplicableFieldFormatter();
+
+    // Don't bother checking if not allowed.
+    if ($access->isAllowed()) {
+      if ($this->getPluginId() === 'entity_reference:entity_reference_entity_view') {
+        // This option disables entity_reference_entity_view plugin for content
+        // entity types. If it is truthy then the plugin is enabled for all
+        // entity types.
+        $mode = $this->configFactory->get('entity_embed.settings')->get('rendered_entity_mode');
+        if ($mode) {
+          // Return *allowed* object.
+          return $access;
+        }
+
+        // Only allow this if this is not a content entity type.
+        $entity_type_id = $this->getEntityTypeFromContext();
+        if ($entity_type_id) {
+          $definition = $this->entityTypeManager->getDefinition($entity_type_id);
+          return $access->andIf(AccessResult::allowedIf(!$definition->entityClassImplements(ContentEntityInterface::class)));
+        }
+      }
+    }
+
+    return $access;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    $build = parent::build();
+
+    // Early return if this derived plugin is not using an EntityViewBuilder.
+    // @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults()
+    if (!isset($build['#view_mode'])) {
+      return $build;
+    }
+
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = $this->getEntityFromContext();
+
+    // There are a few concerns when rendering an embedded media entity:
+    // - entity access checking happens not during rendering but during routing,
+    //   and therefore we have to do it explicitly here for the embedded entity.
+    $build['#access'] = $entity->access('view', NULL, TRUE);
+    // - caching an embedded entity separately is unnecessary; the host entity
+    //   is already render cached; plus specific values may be overridden (such
+    //   as an `alt` attribute) which would mean this particular rendered
+    //   representation is unique to the host entity and hence nonsensical to
+    //   cache separately anyway.
+    unset($build['#cache']['keys']);
+    // - Contextual Links do not make sense for embedded entities; we only allow
+    //   the host entity to be contextually managed.
+    $build['#pre_render'][] = static::class . '::disableContextualLinks';
+    // - Quick Edit does not make sense for embedded entities; we only allow the
+    //   host entity to be edited in-place.
+    $build['#pre_render'][] = static::class . '::disableQuickEdit';
+    // - default styling may break captioned media embeds; attach asset library
+    //   to ensure captions behave as intended.
+    $build['#attached']['library'][] = 'entity_embed/caption';
+
+    return $build;
+  }
+
+  /**
+   * Disables Contextual Links for the embedded media by removing its property.
+   *
+   * @param array $build
+   *   The render array for the embedded media.
+   *
+   * @return array
+   *   The updated render array.
+   *
+   * @see \Drupal\Core\Entity\EntityViewBuilder::addContextualLinks()
+   */
+  public static function disableContextualLinks(array $build) {
+    unset($build['#contextual_links']);
+    return $build;
+  }
+
+  /**
+   * Disables Quick Edit for the embedded media by removing its attributes.
+   *
+   * @param array $build
+   *   The render array for the embedded media.
+   *
+   * @return array
+   *   The updated render array.
+   *
+   * @see quickedit_entity_view_alter()
+   */
+  public static function disableQuickEdit(array $build) {
+    unset($build['#attributes']['data-quickedit-entity-id']);
+    return $build;
   }
 
 }
diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php
index ee29ae3d6a..7101f8512c 100644
--- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php
+++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/FileFieldFormatter.php
@@ -25,7 +25,7 @@ class FileFieldFormatter extends EntityReferenceFieldFormatter {
    */
   public function getFieldValue() {
     $value = parent::getFieldValue();
-    $value += array_intersect_key($this->getConfiguration(), array('description' => ''));
+    $value += array_intersect_key($this->getConfiguration(), ['description' => '']);
     return $value;
   }
 
@@ -47,12 +47,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
 
     // Description is stored in the configuration since it doesn't map to an
     // actual HTML attribute.
-    $form['description'] = array(
+    $form['description'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Description'),
       '#default_value' => $this->getConfigurationValue('description'),
       '#description' => $this->t('The description may be used as the label of the link to the file.'),
-    );
+    ];
 
     return $form;
   }
diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php
index 76b16413ec..c175ded478 100644
--- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php
+++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Image\ImageFactory;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\TypedDataManager;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -36,8 +37,21 @@ class ImageFieldFormatter extends FileFieldFormatter {
   protected $imageFactory;
 
   /**
-   * {@inheritdoc}
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * Constructs an ImageFieldFormatter object.
    *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity manager service.
    * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager
@@ -48,10 +62,13 @@ class ImageFieldFormatter extends FileFieldFormatter {
    *   The image factory.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, TypedDataManager $typed_data_manager, ImageFactory $image_factory, LanguageManagerInterface $language_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, TypedDataManager $typed_data_manager, ImageFactory $image_factory, LanguageManagerInterface $language_manager, MessengerInterface $messenger) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $formatter_plugin_manager, $typed_data_manager, $language_manager);
     $this->imageFactory = $image_factory;
+    $this->messenger = $messenger;
   }
 
   /**
@@ -66,7 +83,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $container->get('plugin.manager.field.formatter'),
       $container->get('typed_data_manager'),
       $container->get('image.factory'),
-      $container->get('language_manager')
+      $container->get('language_manager'),
+      $container->get('messenger')
     );
   }
 
@@ -77,7 +95,7 @@ public function getFieldValue() {
     $value = parent::getFieldValue();
     // File field support descriptions, but images do not.
     unset($value['description']);
-    $value += array_intersect_key($this->getAttributeValues(), array('alt' => '', 'title' => ''));
+    $value += array_intersect_key($this->getAttributeValues(), ['alt' => '', 'title' => '']);
     return $value;
   }
 
@@ -111,7 +129,14 @@ protected function isValidImage() {
       // Loading large files is slow, make sure it is an image mime type before
       // doing that.
       list($type,) = explode('/', $entity->getMimeType(), 2);
-      $access = AccessResult::allowedIf($type == 'image' && $this->imageFactory->get($entity->getFileUri())->isValid())
+      $is_valid_image = FALSE;
+      if ($type == 'image') {
+        $is_valid_image = $this->imageFactory->get($entity->getFileUri())->isValid();
+        if (!$is_valid_image) {
+          $this->messenger->addMessage($this->t('The selected image "@image" is invalid.', ['@image' => $entity->label()]), 'error');
+        }
+      }
+      $access = AccessResult::allowedIf($type == 'image' && $is_valid_image)
         // See the above @todo, this is the best we can do for now.
         ->addCacheableDependency($entity);
     }
@@ -153,29 +178,29 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       // double quotes in place of empty alt text only if that was filled
       // intentionally by the user.
       if (!empty($entity_element) && $entity_element['data-entity-embed-display'] == 'image:image') {
-        $alt = '""';
+        $alt = MediaImageDecorator::EMPTY_STRING;
       }
     }
 
     // Add support for editing the alternate and title text attributes.
-    $form['alt'] = array(
+    $form['alt'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Alternate text'),
       '#default_value' => $alt,
       '#description' => $this->t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'),
-      '#parents' => array('attributes', 'alt'),
+      '#parents' => ['attributes', 'alt'],
       '#required' => TRUE,
       '#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
       '#maxlength' => 512,
-    );
-    $form['title'] = array(
+    ];
+    $form['title'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Title'),
       '#default_value' => $this->getAttributeValue('title', ''),
       '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'),
-      '#parents' => array('attributes', 'title'),
+      '#parents' => ['attributes', 'title'],
       '#maxlength' => 1024,
-    );
+    ];
 
     return $form;
   }
@@ -186,8 +211,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
     // When the alt attribute is set to two double quotes, transform it to the
     // empty string: two double quotes signify "empty alt attribute". See above.
-    if (trim($form_state->getValue(array('attributes', 'alt'))) === '""') {
-      $form_state->setValue(array('attributes', 'alt'), '');
+    if (trim($form_state->getValue(['attributes', 'alt'])) === MediaImageDecorator::EMPTY_STRING) {
+      $form_state->setValue(['attributes', 'alt'], '');
     }
   }
 
diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php
new file mode 100644
index 0000000000..7d319eb9e1
--- /dev/null
+++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface;
+use Drupal\image\Plugin\Field\FieldType\ImageItem;
+use Drupal\media\MediaInterface;
+
+/**
+ * Decorator on all EntityEmbedDisplays that adds alt and title overriding.
+ */
+class MediaImageDecorator implements EntityEmbedDisplayInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * A string that signifies not to render the alt text.
+   *
+   * @const string
+   */
+  const EMPTY_STRING = '""';
+
+  /**
+   * The decorated EntityEmbedDisplay class.
+   *
+   * @var \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface
+   */
+  private $decorated;
+
+  /**
+   * MediaImageDecorator constructor.
+   *
+   * @param \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayInterface $decorated
+   *   The decorated EntityEmbedDisplay plugin.
+   */
+  public function __construct(EntityEmbedDisplayInterface $decorated) {
+    $this->decorated = $decorated;
+  }
+
+  /**
+   * Passes through all unknown calls to the decorated object.
+   */
+  public function __call($method, $args) {
+    return call_user_func_array([$this->decorated, $method], $args);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(AccountInterface $account = NULL) {
+    return $this->decorated->access($account);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    return $this->decorated->validateConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return $this->decorated->defaultConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return $this->decorated->calculateDependencies();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->decorated->getConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginDefinition() {
+    return $this->decorated->getPluginDefinition();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginId() {
+    return $this->decorated->getPluginId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    return $this->decorated->setConfiguration($configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = $this->decorated->buildConfigurationForm($form, $form_state);
+
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = $this->decorated->getEntityFromContext();
+
+    if ($image_field = $this->getMediaImageSourceField($entity)) {
+
+      $settings = $entity->{$image_field}->getItemDefinition()->getSettings();
+      $attributes = $this->getAttributeValues();
+
+      $alt = isset($attributes['alt']) ? $attributes['alt'] : NULL;
+      $title = isset($attributes['title']) ? $attributes['title'] : NULL;
+
+      // Setting empty alt to double quotes. See ImageFieldFormatter.
+      if ($settings['alt_field_required'] && $alt === '') {
+        $alt = static::EMPTY_STRING;
+      }
+
+      if (!empty($settings['alt_field'])) {
+        // Add support for editing the alternate and title text attributes.
+        $form['alt'] = [
+          '#type' => 'textfield',
+          '#title' => $this->t('Alternate text'),
+          '#default_value' => $alt,
+          '#description' => $this->t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'),
+          '#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
+          '#maxlength' => 512,
+          '#placeholder' => $entity->{$image_field}->alt,
+        ];
+      }
+
+      if (!empty($settings['title_field'])) {
+        $form['title'] = [
+          '#type' => 'textfield',
+          '#title' => $this->t('Title'),
+          '#default_value' => $title,
+          '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'),
+          '#maxlength' => 1024,
+          '#placeholder' => $entity->{$image_field}->title,
+        ];
+      }
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = $this->decorated->getEntityFromContext();
+    if ($image_field = $this->getMediaImageSourceField($entity)) {
+      $settings = $entity->{$image_field}->getItemDefinition()->getSettings();
+      $values = $form_state->getValue(['attributes', 'data-entity-embed-display-settings']);
+
+      if (!empty($settings['alt_field'])) {
+        // When the alt attribute is set to two double quotes, transform it to
+        // the empty string: two double quotes signify "empty alt attribute".
+        // See ImagefieldFormatter.
+        if (trim($values['alt']) === static::EMPTY_STRING) {
+          $values['alt'] = static::EMPTY_STRING;
+        }
+        // If the alt text is unchanged from the values set on the
+        // field, there's no need for the alt property to be set.
+        elseif ($values['alt'] === $entity->{$image_field}->alt) {
+          $values['alt'] = '';
+        }
+
+        $form_state->setValue(['attributes', 'alt'], $values['alt']);
+        $form_state->unsetValue([
+          'attributes',
+          'data-entity-embed-display-settings',
+          'alt',
+        ]);
+      }
+
+      if (!empty($settings['title_field'])) {
+        if (empty($values['title'])) {
+          $values['title'] = '';
+        }
+        // If the title text is unchanged from the values set on the
+        // field, there's no need for the title property to be set.
+        elseif ($values['title'] === $entity->{$image_field}->title) {
+          $values['title'] = '';
+        }
+
+        $form_state->setValue(['attributes', 'title'], $values['title']);
+        $form_state->unsetValue([
+          'attributes',
+          'data-entity-embed-display-settings',
+          'title',
+        ]);
+      }
+    }
+    $this->decorated->submitConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    $build = $this->decorated->build();
+
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = $this->decorated->getEntityFromContext();
+
+    if ($image_field = $this->getMediaImageSourceField($entity)) {
+      $settings = $entity->{$image_field}->getItemDefinition()->getSettings();
+
+      if (!empty($settings['alt_field']) && $this->hasAttribute('alt')) {
+        $entity->{$image_field}->alt = $this->getAttributeValue('alt');
+        $entity->thumbnail->alt = $this->getAttributeValue('alt');
+      }
+
+      if (!empty($settings['title_field']) && $this->hasAttribute('title')) {
+        $entity->{$image_field}->title = $this->getAttributeValue('title');
+        $entity->thumbnail->title = $this->getAttributeValue('title');
+      }
+    }
+
+    return $build;
+  }
+
+  /**
+   * Get image field from source config.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   Embedded entity.
+   *
+   * @return string|null
+   *   String of image field name.
+   */
+  protected function getMediaImageSourceField(EntityInterface $entity) {
+    if (!$entity instanceof MediaInterface) {
+      return NULL;
+    }
+
+    $field_definition = $entity->getSource()
+      ->getSourceFieldDefinition($entity->bundle->entity);
+    $item_class = $field_definition->getItemDefinition()->getClass();
+    if ($item_class == ImageItem::class || is_subclass_of($item_class, ImageItem::class)) {
+      return $field_definition->getName();
+    }
+    return NULL;
+  }
+
+}
diff --git a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php
index 14a8a6a70f..b69dae831f 100644
--- a/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php
+++ b/web/modules/entity_embed/src/Plugin/entity_embed/EntityEmbedDisplay/ViewModeFieldFormatter.php
@@ -59,4 +59,28 @@ public function getFieldFormatterId() {
     return 'entity_reference_entity_view';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    $definition = $this->getPluginDefinition();
+    $view_mode = $definition['view_mode'];
+
+    $view_modes = [];
+
+    foreach ($definition['entity_types'] as $type) {
+      $view_modes[] = "$type.$view_mode";
+    }
+
+    $entity_view_modes = $this->entityTypeManager
+      ->getStorage('entity_view_mode')
+      ->loadMultiple($view_modes);
+
+    foreach ($entity_view_modes as $view_mode) {
+      $this->addDependency($view_mode->getConfigDependencyKey(), $view_mode->getConfigDependencyName());
+    }
+
+    return $this->dependencies;
+  }
+
 }
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php b/web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php
deleted file mode 100644
index b7d84d9b87..0000000000
--- a/web/modules/entity_embed/src/Tests/EntityEmbedDialogTest.php
+++ /dev/null
@@ -1,172 +0,0 @@
-<?php
-
-namespace Drupal\entity_embed\Tests;
-
-use Drupal\editor\Entity\Editor;
-
-/**
- * Tests the entity_embed dialog controller and route.
- *
- * @group entity_embed
- */
-class EntityEmbedDialogTest extends EntityEmbedTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['image'];
-
-  /**
-   * Tests the entity embed dialog.
-   */
-  public function testEntityEmbedDialog() {
-    // Ensure that the route is not accessible without specifying all the
-    // parameters.
-    $this->getEmbedDialog();
-    $this->assertResponse(404, 'Embed dialog is not accessible without specifying filter format and embed button.');
-    $this->getEmbedDialog('custom_format');
-    $this->assertResponse(404, 'Embed dialog is not accessible without specifying embed button.');
-
-    // Ensure that the route is not accessible with an invalid embed button.
-    $this->getEmbedDialog('custom_format', 'invalid_button');
-    $this->assertResponse(404, 'Embed dialog is not accessible without specifying filter format and embed button.');
-
-    // Ensure that the route is not accessible with text format without the
-    // button configured.
-    $this->getEmbedDialog('plain_text', 'node');
-    $this->assertResponse(404, 'Embed dialog is not accessible with a filter that does not have an editor configuration.');
-
-    // Add an empty configuration for the plain_text editor configuration.
-    $editor = Editor::create([
-      'format' => 'plain_text',
-      'editor' => 'ckeditor',
-    ]);
-    $editor->save();
-    $this->getEmbedDialog('plain_text', 'node');
-    $this->assertResponse(403, 'Embed dialog is not accessible with a filter that does not have the embed button assigned to it.');
-
-    // Ensure that the route is accessible with a valid embed button.
-    // 'Node' embed button is provided by default by the module and hence the
-    // request must be successful.
-    $this->getEmbedDialog('custom_format', 'node');
-    $this->assertResponse(200, 'Embed dialog is accessible with correct filter format and embed button.');
-
-    // Ensure form structure of the 'select' step and submit form.
-    $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.');
-
-    // $edit = ['attributes[data-entity-id]' => $this->node->id()];
-    // $this->drupalPostAjaxForm(NULL, $edit, 'op');
-    // Ensure form structure of the 'embed' step and submit form.
-    // $this->assertFieldByName('attributes[data-entity-embed-display]', 'Entity Embed Display plugin field is present.');.
-  }
-
-  /**
-   * Tests the entity embed button markup.
-   */
-  public function testEntityEmbedButtonMarkup() {
-    // Ensure that the route is not accessible with text format without the
-    // button configured.
-    $this->getEmbedDialog('plain_text', 'node');
-    $this->assertResponse(404, 'Embed dialog is not accessible with a filter that does not have an editor configuration.');
-
-    // Add an empty configuration for the plain_text editor configuration.
-    $editor = Editor::create([
-      'format' => 'plain_text',
-      'editor' => 'ckeditor',
-    ]);
-    $editor->save();
-    $this->getEmbedDialog('plain_text', 'node');
-    $this->assertResponse(403, 'Embed dialog is not accessible with a filter that does not have the embed button assigned to it.');
-
-    // Ensure that the route is accessible with a valid embed button.
-    // 'Node' embed button is provided by default by the module and hence the
-    // request must be successful.
-    $this->getEmbedDialog('custom_format', 'node');
-    $this->assertResponse(200, 'Embed dialog is accessible with correct filter format and embed button.');
-
-    // Ensure form structure of the 'select' step and submit form.
-    $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.');
-
-    // Check that 'Next' is a primary button.
-    $this->assertFieldByXPath('//input[contains(@class, "button--primary")]', 'Next', 'Next is a primary button');
-
-    $title = $this->node->getTitle() . ' (' . $this->node->id() . ')';
-    $edit = ['entity_id' => $title];
-    $response = $this->drupalPostAjaxForm(NULL, $edit, 'op');
-    $plugins = [
-      'entity_reference:entity_reference_label',
-      'entity_reference:entity_reference_entity_id',
-      'view_mode:node.full',
-      'view_mode:node.rss',
-      'view_mode:node.search_index',
-      'view_mode:node.search_result',
-      'view_mode:node.teaser',
-    ];
-    foreach ($plugins as $plugin) {
-      $this->assertTrue(strpos($response[2]['data'], $plugin), 'Plugin ' . $plugin . ' is available in selection.');
-    }
-
-    $this->container->get('config.factory')->getEditable('entity_embed.settings')
-      ->set('rendered_entity_mode', TRUE)->save();
-    $this->container->get('plugin.manager.entity_embed.display')->clearCachedDefinitions();
-
-    $this->getEmbedDialog('custom_format', 'node');
-    $title = $this->node->getTitle() . ' (' . $this->node->id() . ')';
-    $edit = ['entity_id' => $title];
-    $response = $this->drupalPostAjaxForm(NULL, $edit, 'op');
-
-    $plugins = [
-      'entity_reference:entity_reference_label',
-      'entity_reference:entity_reference_entity_id',
-      'entity_reference:entity_reference_entity_view',
-    ];
-    foreach ($plugins as $plugin) {
-      $this->assertTrue(strpos($response[2]['data'], $plugin), 'Plugin ' . $plugin . ' is available in selection.');
-    }
-    /*$this->drupalPostForm(NULL, $edit, 'Next');
-    // Ensure form structure of the 'embed' step and submit form.
-    $this->assertFieldByName('attributes[data-entity-embed-display]', 'Entity Embed Display plugin field is present.');
-
-    // Check that 'Embed' is a primary button.
-    $this->assertFieldByXPath('//input[contains(@class, "button--primary")]', 'Embed', 'Embed is a primary button');*/
-  }
-
-  /**
-   * Tests entity embed functionality.
-   */
-  public function testEntityEmbedFunctionality() {
-    $edit = [
-      'entity_id' => $this->node->getTitle() . ' (' . $this->node->id() . ')',
-    ];
-    $this->getEmbedDialog('custom_format', 'node');
-    $this->drupalPostForm(NULL, $edit, t('Next'));
-    // Tests that the embed dialog doesn't trow a fatal in
-    // ImageFieldFormatter::isValidImage()
-    $this->assertResponse(200);
-  }
-
-  /**
-   * Retrieves an embed dialog based on given parameters.
-   *
-   * @param string $filter_format_id
-   *   ID of the filter format.
-   * @param string $embed_button_id
-   *   ID of the embed button.
-   *
-   * @return string
-   *   The retrieved HTML string.
-   */
-  public function getEmbedDialog($filter_format_id = NULL, $embed_button_id = NULL) {
-    $url = 'entity-embed/dialog';
-    if (!empty($filter_format_id)) {
-      $url .= '/' . $filter_format_id;
-      if (!empty($embed_button_id)) {
-        $url .= '/' . $embed_button_id;
-      }
-    }
-    return $this->drupalGet($url);
-  }
-
-}
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php b/web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php
deleted file mode 100644
index 594ff953bf..0000000000
--- a/web/modules/entity_embed/src/Tests/EntityEmbedFilterTest.php
+++ /dev/null
@@ -1,198 +0,0 @@
-<?php
-
-namespace Drupal\entity_embed\Tests;
-
-/**
- * Tests the entity_embed filter.
- *
- * @group entity_embed
- */
-class EntityEmbedFilterTest extends EntityEmbedTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = [
-    'file',
-    'image',
-    'entity_embed',
-    'entity_embed_test',
-    'node',
-    'ckeditor',
-  ];
-
-  /**
-   * 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() {
-    // 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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-id and view-mode';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertText($this->node->body->value, 'Embedded node exists in page');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity">', 'Embed container found.');
-
-    // 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->assertNoRaw('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertNoText($this->node->body->value, 'Embedded node does not exist in the page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    // 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->assertNoRaw('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertText($this->node->body->value, 'Embedded node exists in the page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity">', 'Embed container found.');
-    $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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-uuid and view-mode';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertText($this->node->body->value, 'Embedded node exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity">', 'Embed container found.');
-    $this->assertCacheTag('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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test that placeholder is retained when specified entity does not exists';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is unsuccessful.');
-
-    // 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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test that entity-uuid is preferred over entity-id when both attributes are present';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertNoRaw('<drupal-entity data-entity-type="node" data-entity');
-    $this->assertText($this->node->body->value, 'Entity specifed with UUID exists in the page.');
-    $this->assertNoText($sample_node->body->value, 'Entity specifed with ID does not exists in the page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity"', 'Embed container found.');
-
-    // 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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertText($this->node->body->value, 'Embedded node exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity"', 'Embed container found.');
-
-    // 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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'Test entity embed with entity-embed-display and data-entity-embed-display-settings';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalGet('node/' . $node->id());
-    $this->assertText($this->node->body->value, 'Embedded node exists in page with the view mode specified by entity-embed-settings.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity"', 'Embed container found.');
-
-    // Ensure the embedded node doesn't contain data tags on the full page.
-    $this->drupalGet('node/' . $this->node->id());
-    $this->assertNoRaw('data-align="left"', 'Align data attribute not found.');
-    $this->assertNoRaw('data-caption="test caption"', 'Caption data attribute not found.');
-
-    // 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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'test entity embed with entity-id and view-mode';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalget('node/' . $node->id());
-    $this->assertNoText($this->node->body->value, 'embedded node exists in page');
-    $this->assertRaw('</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 = array();
-    $settings['type'] = 'page';
-    $settings['title'] = 'test entity embed with entity-id and view-mode';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
-    $node = $this->drupalCreateNode($settings);
-    $this->drupalget('node/' . $node->id());
-    $this->assertNoText($this->node->body->value, 'embedded node exists in page');
-    $this->assertRaw('<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->assertRaw('<img src', 'IMG tag found.');
-    $this->assertRaw('data-caption="test caption"', 'Caption found.');
-    $this->assertRaw('data-align="left"', 'Alignment information found.');
-    $this->assertTrue((bool) $this->xpath("//img[@alt='This is alt text']"), 'Alt text found');
-    $this->assertTrue((bool) $this->xpath("//img[@title='This is title text']"), 'Title text found');
-    $this->assertRaw('<article class="embedded-entity"', 'Embed container found.');
-
-    // 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->assertText($this->node->getTitle(), 'Embeded node title is displayed.');
-    $this->assertNoLink($this->node->getTitle(), 'Embed settings are respected.');
-    $this->assertNoText($this->node->body->value, 'Embedded node exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appear in the output when embed is successful.');
-    $this->assertRaw('<article class="embedded-entity"', 'Embed container found.');
-  }
-
-}
diff --git a/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php b/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php
index 28ef108cb8..5dac420b72 100644
--- a/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php
+++ b/web/modules/entity_embed/src/Twig/EntityEmbedTwigExtension.php
@@ -30,6 +30,8 @@ class EntityEmbedTwigExtension extends \Twig_Extension {
    *
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager service.
+   * @param \Drupal\entity_embed\EntityEmbedBuilderInterface $builder
+   *   The Entity embed builder service.
    */
   public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityEmbedBuilderInterface $builder) {
     $this->entityTypeManager = $entity_type_manager;
@@ -57,9 +59,9 @@ public function getName() {
    * {@inheritdoc}
    */
   public function getFunctions() {
-    return array(
+    return [
       new \Twig_SimpleFunction('entity_embed', [$this, 'getRenderArray']),
-    );
+    ];
   }
 
   /**
diff --git a/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php b/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php
index e847f5294b..860975cba9 100644
--- a/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php
+++ b/web/modules/entity_embed/tests/fixtures/update/entity_embed.update-hook-test.php
@@ -2,6 +2,7 @@
 
 /**
  * @file
+ * Update hook test file.
  */
 
 use Drupal\Core\Database\Database;
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml
new file mode 100644
index 0000000000..f8a6bfe4de
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml
@@ -0,0 +1,42 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.article.body
+    - node.type.article
+  module:
+    - text
+id: node.article.default
+targetEntityType: node
+bundle: article
+mode: default
+content:
+  title:
+    type: string_textfield
+    weight: 1
+    region: content
+    settings:
+      size: 60
+      placeholder: ''
+    third_party_settings: {  }
+  body:
+    type: text_textarea_with_summary
+    weight: 2
+    settings:
+      rows: 9
+      summary_rows: 3
+      placeholder: ''
+    third_party_settings: {  }
+    region: content
+  status:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 3
+    region: content
+    third_party_settings: {  }
+hidden:
+  created: true
+  promote: true
+  sticky: true
+  uid: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml
new file mode 100644
index 0000000000..60ae55d6a5
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml
@@ -0,0 +1,29 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.media.image.field_media_image
+    - image.style.medium
+    - media.type.image
+  module:
+    - image
+    - user
+id: media.image.default
+targetEntityType: media
+bundle: image
+mode: embed
+content:
+  field_media_image:
+    type: image
+    weight: 2
+    region: content
+    label: hidden
+    settings:
+      image_style: medium
+      image_link: ''
+    third_party_settings: {  }
+hidden:
+  name: true
+  thumbnail: true
+  created: true
+  uid: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml
new file mode 100644
index 0000000000..4c4338c028
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml
@@ -0,0 +1,28 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - image.style.medium
+    - media.type.image
+  module:
+    - image
+    - user
+id: media.image.default
+targetEntityType: media
+bundle: image
+mode: thumb
+content:
+  thumbnail:
+    type: image
+    weight: 2
+    region: content
+    label: hidden
+    settings:
+      image_style: medium
+      image_link: ''
+    third_party_settings: {  }
+hidden:
+  name: true
+  field_media_image: true
+  created: true
+  uid: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml
new file mode 100644
index 0000000000..edf834f62d
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml
@@ -0,0 +1,27 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.article.body
+    - node.type.article
+  module:
+    - text
+    - user
+id: node.article.default
+targetEntityType: node
+bundle: article
+mode: default
+content:
+  body:
+    label: hidden
+    type: text_default
+    weight: 101
+    settings: {  }
+    third_party_settings: {  }
+    region: content
+  links:
+    weight: 100
+    settings: {  }
+    third_party_settings: {  }
+    region: content
+hidden: {  }
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml
new file mode 100644
index 0000000000..6a87a051b4
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: false
+dependencies:
+  module:
+    - media
+id: media.embed
+label: 'Embed'
+targetEntityType: media
+cache: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml
new file mode 100644
index 0000000000..54f487c9d0
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: false
+dependencies:
+  module:
+    - media
+id: media.thumb
+label: 'Thumb (View Mode)'
+targetEntityType: media
+cache: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml
new file mode 100644
index 0000000000..4468000706
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml
@@ -0,0 +1,64 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - filter.format.full_html
+  module:
+    - ckeditor
+format: full_html
+editor: ckeditor
+settings:
+  toolbar:
+    rows:
+      -
+        -
+          name: Formatting
+          items:
+            - Bold
+            - Italic
+            - Strike
+            - Superscript
+            - Subscript
+            - '-'
+            - RemoveFormat
+        -
+          name: Linking
+          items:
+            - DrupalLink
+            - DrupalUnlink
+        -
+          name: Lists
+          items:
+            - BulletedList
+            - NumberedList
+        -
+          name: Media
+          items:
+            - Blockquote
+            - DrupalImage
+            - Table
+            - HorizontalRule
+            - test_node
+            - test_media_entity_embed
+        -
+          name: 'Block Formatting'
+          items:
+            - Format
+        -
+          name: Tools
+          items:
+            - ShowBlocks
+            - Source
+  plugins:
+    language:
+      language_list: un
+    stylescombo:
+      styles: ''
+image_upload:
+  status: true
+  scheme: public
+  directory: inline-images
+  max_size: ''
+  max_dimensions:
+    width: null
+    height: null
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml
new file mode 100644
index 0000000000..7373631bd0
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml
@@ -0,0 +1,21 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - media.type.image
+  module:
+    - entity_embed
+    - media
+label: 'Media Entity Embed'
+id: test_media_entity_embed
+type_id: entity
+type_settings:
+  entity_type: media
+  bundles:
+    - image
+  display_plugins: {  }
+  entity_browser: ''
+  entity_browser_settings:
+    display_review: false
+icon_uuid: null
+icon_path: null
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml
new file mode 100644
index 0000000000..db25535602
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml
@@ -0,0 +1,15 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - entity_embed
+    - node
+label: Node
+id: test_node
+type_id: entity
+type_settings:
+  entity_type: node
+  bundles: {  }
+  display_plugins: {  }
+icon_uuid: null
+icon_path: null
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml
new file mode 100644
index 0000000000..d625807b7d
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml
@@ -0,0 +1,40 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.media.field_media_image
+    - media.type.image
+  enforced:
+    module:
+      - media
+  module:
+    - image
+id: media.image.field_media_image
+field_name: field_media_image
+entity_type: media
+bundle: image
+label: Image
+description: ''
+required: true
+translatable: true
+default_value: {  }
+default_value_callback: ''
+settings:
+  alt_field: true
+  alt_field_required: true
+  title_field: true
+  title_field_required: false
+  max_resolution: ''
+  min_resolution: ''
+  default_image:
+    uuid: null
+    alt: ''
+    title: ''
+    width: null
+    height: null
+  file_directory: '[date:custom:Y]-[date:custom:m]'
+  file_extensions: 'png gif jpg jpeg'
+  max_filesize: ''
+  handler: 'default:file'
+  handler_settings: {  }
+field_type: image
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml
new file mode 100644
index 0000000000..8f3681d962
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml
@@ -0,0 +1,21 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.node.body
+    - node.type.article
+  module:
+    - text
+id: node.article.body
+field_name: body
+entity_type: node
+bundle: article
+label: Body
+description: ''
+required: false
+translatable: true
+default_value: {  }
+default_value_callback: ''
+settings:
+  display_summary: true
+field_type: text_with_summary
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml
new file mode 100644
index 0000000000..231200d59b
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml
@@ -0,0 +1,32 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - media
+  module:
+    - file
+    - image
+    - media
+id: media.field_media_image
+field_name: field_media_image
+entity_type: media
+type: image
+settings:
+  default_image:
+    uuid: null
+    alt: ''
+    title: ''
+    width: null
+    height: null
+  target_type: file
+  display_field: false
+  display_default: false
+  uri_scheme: public
+module: image
+locked: false
+cardinality: 1
+translatable: true
+indexes: {  }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml
new file mode 100644
index 0000000000..9b9f864ffe
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml
@@ -0,0 +1,49 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - editor
+    - entity_embed
+name: 'Full HTML'
+format: full_html
+weight: 10
+filters:
+  filter_align:
+    id: filter_align
+    provider: filter
+    status: true
+    weight: 8
+    settings: {  }
+  filter_caption:
+    id: filter_caption
+    provider: filter
+    status: true
+    weight: 9
+    settings: {  }
+  filter_htmlcorrector:
+    id: filter_htmlcorrector
+    provider: filter
+    status: true
+    weight: 10
+    settings: {  }
+  editor_file_reference:
+    id: editor_file_reference
+    provider: editor
+    status: true
+    weight: 11
+    settings: {  }
+  entity_embed:
+    id: entity_embed
+    provider: entity_embed
+    status: true
+    weight: 0
+    settings: {  }
+  filter_html:
+    id: filter_html
+    provider: filter
+    status: true
+    weight: -10
+    settings:
+      allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <s> <sup> <sub> <img src alt data-entity-type data-entity-uuid data-align data-caption> <table> <caption> <tbody> <thead> <tfoot> <th> <td> <tr> <hr> <p> <h1> <pre> <drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title>'
+      filter_html_help: true
+      filter_html_nofollow: false
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml
new file mode 100644
index 0000000000..3a53b77365
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/media.type.image.yml
@@ -0,0 +1,12 @@
+langcode: en
+status: true
+dependencies: {  }
+id: image
+label: Image
+description: 'Use local images for reusable media.'
+source: image
+queue_thumbnail_downloads: false
+new_revision: true
+source_configuration:
+  source_field: field_media_image
+field_map: {  }
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml
new file mode 100644
index 0000000000..a65ff908d0
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/config/install/node.type.article.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies: {  }
+name: 'Article'
+type: article
+description: 'Article'
+help: ''
+new_revision: false
+preview_mode: 1
+display_submitted: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml
index 5ffb92825f..2c0ac91d49 100644
--- a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.info.yml
@@ -1,12 +1,19 @@
 name: 'Entity Embed test'
 type: module
 description: 'Support module for the Entity Embed module tests.'
-# core: 8.x
 package: Testing
-# version: VERSION
+dependencies:
+ - drupal:file
+ - drupal:image
+ - drupal:node
+ - drupal:text
+ - drupal:media
+ - drupal:ckeditor
+ - drupal:editor
+ - embed:embed
+ - entity_embed:entity_embed
 
-# Information added by Drupal.org packaging script on 2016-10-17
-version: '8.x-1.0-beta2'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-03-26
+version: '8.x-1.1'
 project: 'entity_embed'
-datestamp: 1476698379
+datestamp: 1585252809
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module b/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module
index b34a430c02..a8ecd923fb 100644
--- a/web/modules/entity_embed/tests/modules/entity_embed_test/entity_embed_test.module
+++ b/web/modules/entity_embed/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;
 
@@ -13,15 +14,15 @@
  * Implements hook_theme().
  */
 function entity_embed_test_theme($existing, $type, $theme, $path) {
-  $items['entity_embed_twig_test'] = array(
+  $items['entity_embed_twig_test'] = [
     'template' => 'entity_embed_twig_test',
-    'variables' => array(
+    'variables' => [
       'entity_type' => '',
       'id' => '',
       'display_plugin' => 'default',
-      'display_settings' => array(),
-    ),
-  );
+      'display_settings' => [],
+    ],
+  ];
   return $items;
 }
 
@@ -35,7 +36,7 @@ function entity_embed_test_entity_embed_display_plugins_alter(&$info) {
   }
 
   // Prefix each plugin name with 'testing_hook:'.
-  $new_info = array();
+  $new_info = [];
   foreach ($info as $key => $value) {
     $new_key = "testing_hook:" . $key;
     $new_info[$new_key] = $info[$key];
@@ -55,7 +56,7 @@ function entity_embed_test_entity_embed_context_alter(array &$context, EntityInt
 
   // Force to use 'Label' plugin.
   $context['data-entity-embed-display'] = 'entity_reference:entity_reference_label';
-  $context['data-entity-embed-display-settings'] = array('link' => 1);
+  $context['data-entity-embed-display-settings'] = ['link' => 1];
 
   // Set title of the entity.
   $entity->setTitle("Title set by hook_entity_embed_context_alter");
@@ -70,6 +71,9 @@ function entity_embed_test_entity_embed_alter(array &$build, EntityInterface $en
     return;
   }
 
+  // Adding classes is as simple as appending to an array.
+  $build['#attributes']['class'][] = 'test-class-added-in-alter-hook';
+
   // Set title of the 'node' entity.
   $entity->setTitle("Title set by hook_entity_embed_alter");
 }
@@ -82,3 +86,12 @@ 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();
+  // @see \Drupal\Tests\entity_embed\FunctionalJavascript\MediaImageTest::testPreviewUsesDefaultTheme()
+  $build['#attributes']['data-entity-embed-test-active-theme'] = \Drupal::theme()->getActiveTheme()->getName();
+}
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php b/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php
index 7c60f948a4..d0eb4fe02c 100644
--- a/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php
+++ b/web/modules/entity_embed/tests/modules/entity_embed_test/src/EntityEmbedTestTwigController.php
@@ -11,36 +11,42 @@ class EntityEmbedTestTwigController {
    * Menu callback for testing entity_embed twig extension using entity ID.
    */
   public function idRender() {
-    return array(
+    return [
       '#theme' => 'entity_embed_twig_test',
       '#entity_type' => 'node',
       '#id' => '1',
-    );
+    ];
   }
 
   /**
-   * Menu callback for testing entity_embed twig extension using 'label' Entity Embed Display plugin.
+   * Menu callback.
+   *
+   * Used for testing entity_embed twig extension using 'label' Entity Embed
+   * Display plugin.
    */
   public function labelPluginRender() {
-    return array(
+    return [
       '#theme' => 'entity_embed_twig_test',
       '#entity_type' => 'node',
       '#id' => '1',
       '#display_plugin' => 'entity_reference:entity_reference_label',
-    );
+    ];
   }
 
   /**
-   * Menu callback for testing entity_embed twig extension using 'label' Entity Embed Display plugin without linking to the node.
+   * Menu callback.
+   *
+   * Used for testing entity_embed twig extension using 'label' Entity Embed
+   * Display plugin without linking to the node.
    */
   public function labelPluginNoLinkRender() {
-    return array(
+    return [
       '#theme' => 'entity_embed_twig_test',
       '#entity_type' => 'node',
       '#id' => '1',
       '#display_plugin' => 'entity_reference:entity_reference_label',
-      '#display_settings' => array('link' => 0),
-    );
+      '#display_settings' => ['link' => 0],
+    ];
   }
 
 }
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml
new file mode 100644
index 0000000000..653a78e661
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.media.image.yml
@@ -0,0 +1,15 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - media.type.image
+  module:
+    - content_translation
+third_party_settings:
+  content_translation:
+    enabled: true
+id: media.image
+target_entity_type_id: media
+target_bundle: image
+default_langcode: site_default
+language_alterable: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml
new file mode 100644
index 0000000000..de684ef1f4
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.content_settings.node.article.yml
@@ -0,0 +1,15 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - node.type.article
+  module:
+    - content_translation
+third_party_settings:
+  content_translation:
+    enabled: true
+id: node.article
+target_entity_type_id: node
+target_bundle: article
+default_langcode: site_default
+language_alterable: true
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml
new file mode 100644
index 0000000000..280469e139
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/config/install/language.entity.fr.yml
@@ -0,0 +1,8 @@
+langcode: en
+status: true
+dependencies: {  }
+id: fr
+label: French
+direction: ltr
+weight: 1
+locked: false
diff --git a/web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml
new file mode 100644
index 0000000000..3daeda6ddd
--- /dev/null
+++ b/web/modules/entity_embed/tests/modules/entity_embed_translation_test/entity_embed_translation_test.info.yml
@@ -0,0 +1,20 @@
+name: 'Entity Embed Translation test'
+type: module
+description: 'Aids in testing translation within entity embed'
+package: Testing
+dependencies:
+ - drupal:content_translation
+ - drupal:file
+ - drupal:image
+ - drupal:node
+ - drupal:text
+ - drupal:media
+ - drupal:ckeditor
+ - drupal:editor
+ - embed:embed
+ - entity_embed:entity_embed
+
+# Information added by Drupal.org packaging script on 2020-03-26
+version: '8.x-1.1'
+project: 'entity_embed'
+datestamp: 1585252809
diff --git a/web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php
new file mode 100644
index 0000000000..67d305c0f2
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDialogTest.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Functional;
+
+use Drupal\editor\Entity\Editor;
+
+/**
+ * Tests the entity_embed dialog controller and route.
+ *
+ * @group entity_embed
+ */
+class EntityEmbedDialogTest extends EntityEmbedTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['image'];
+
+  /**
+   * Tests the entity embed dialog.
+   */
+  public function testEntityEmbedDialog() {
+    // Ensure that the route is not accessible without specifying all the
+    // parameters.
+    $this->drupalGet('/entity-embed/dialog');
+    // Verify embed dialog is not accessible without specifying filter format
+    // and embed button.
+    $this->assertSession()->statusCodeEquals(404);
+    $this->drupalGet('/entity-embed/dialog/custom_format');
+    // Verify embed dialog is not accessible without specifying embed button.
+    $this->assertSession()->statusCodeEquals(404);
+
+    // Ensure that the route is not accessible with an invalid embed button.
+    $this->drupalGet('/entity-embed/dialog/custom_format/invalid_button');
+    // Verify embed dialog is not accessible without specifying filter format
+    // and embed button.
+    $this->assertSession()->statusCodeEquals(404);
+
+    // Ensure that the route is not accessible with text format without the
+    // button configured.
+    $this->drupalGet('/entity-embed/dialog/plain_text/node');
+    // Verify embed dialog is not accessible with a filter that does not have
+    // an editor configuration.
+    $this->assertSession()->statusCodeEquals(404);
+
+    // Add an empty configuration for the plain_text editor configuration.
+    $editor = Editor::create([
+      'format' => 'plain_text',
+      'editor' => 'ckeditor',
+    ]);
+    $editor->save();
+    $this->drupalGet('/entity-embed/dialog/plain_text/node');
+    // Verify embed dialog is not accessible with a filter that does not have
+    // the embed button assigned to it.
+    $this->assertSession()->statusCodeEquals(403);
+
+    // Ensure that the route is accessible with a valid embed button.
+    // 'Node' embed button is provided by default by the module and hence the
+    // request must be successful.
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+    // Verify embed dialog is accessible with correct filter format
+    // and embed button.
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Ensure form structure of the 'select' step and submit form.
+    $this->assertSession()->fieldExists('entity_id');
+  }
+
+  /**
+   * Tests the entity embed button markup.
+   */
+  public function testEntityEmbedButtonMarkup() {
+    // Ensure that the route is not accessible with text format without the
+    // button configured.
+    $this->drupalGet('/entity-embed/dialog/plain_text/node');
+    // Verify embed dialog is not accessible with a filter that does not have
+    // an editor configuration.
+    $this->assertSession()->statusCodeEquals(404);
+
+    // Add an empty configuration for the plain_text editor configuration.
+    $editor = Editor::create([
+      'format' => 'plain_text',
+      'editor' => 'ckeditor',
+    ]);
+    $editor->save();
+    $this->drupalGet('/entity-embed/dialog/plain_text/node');
+    // Verify embed dialog is not accessible with a filter that does not have
+    // the embed button assigned to it.
+    $this->assertSession()->statusCodeEquals(403);
+
+    // Ensure that the route is accessible with a valid embed button.
+    // 'Node' embed button is provided by default by the module and hence the
+    // request must be successful.
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+    // Verify embed dialog is accessible with correct filter format
+    // and embed button.
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Ensure form structure of the 'select' step and submit form.
+    $this->assertSession()->fieldExists('entity_id');
+
+    // Check that 'Next' is a primary button.
+    $this->assertSession()->elementExists('xpath', '//input[contains(@class, "button--primary")]');
+  }
+
+  /**
+   * Tests entity embed functionality.
+   */
+  public function testEntityEmbedFunctionality() {
+    $edit = [
+      'entity_id' => $this->node->getTitle() . ' (' . $this->node->id() . ')',
+    ];
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+    $this->drupalPostForm(NULL, $edit, t('Next'));
+    // Tests that the embed dialog doesn't trow a fatal in
+    // ImageFieldFormatter::isValidImage()
+    $this->assertSession()->statusCodeEquals(200);
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php
new file mode 100644
index 0000000000..dbd5067cd4
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedDisplayManagerTest.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager
+ * @group entity_embed
+ */
+class EntityEmbedDisplayManagerTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'field_ui',
+    'node',
+    'file',
+    'image',
+    'ckeditor',
+    'entity_embed',
+  ];
+
+  /**
+   * The test button that embeds image files.
+   *
+   * @var \Drupal\embed\Entity\EmbedButton
+   */
+  protected $imageButton;
+
+  /**
+   * Created file entity.
+   *
+   * @var \Drupal\file\FileInterface
+   */
+  protected $image;
+
+  /**
+   * File created with invalid image.
+   *
+   * @var \Drupal\file\FileInterface
+   */
+  protected $invalidImage;
+
+  /**
+   * The EntityEmbedDisplay plugin manager.
+   *
+   * @var \Drupal\entity_embed\EntityEmbedDisplay\EntityEmbedDisplayManager
+   */
+  protected $entityEmbedDisplayManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->imageButton = $this->container->get('entity_type.manager')
+      ->getStorage('embed_button')
+      ->create([
+        'label' => 'Image Embed',
+        'id' => 'image_embed',
+        'type_id' => 'entity',
+        'type_settings' => [
+          'entity_type' => 'file',
+          'display_plugins' => [
+            'image:image',
+          ],
+          'entity_browser' => '',
+          'entity_browser_settings' => [
+            'display_review' => FALSE,
+          ],
+        ],
+        'icon_uuid' => NULL,
+      ]);
+    $this->imageButton->save();
+
+    // Create a sample image to embed.
+    $entity_embed_path = $this->container->get('module_handler')
+      ->getModule('entity_embed')
+      ->getPath();
+    \Drupal::service('file_system')->copy($entity_embed_path . '/js/plugins/drupalentity/entity.png', 'public://example1.png');
+
+    // Resize the test image so that it will be scaled down during token
+    // replacement.
+    $this->image1 = $this->container->get('image.factory')->get('public://example1.png');
+    $this->image1->resize(500, 500);
+    $this->image1->save();
+
+    $this->image = $this->container->get('entity_type.manager')
+      ->getStorage('file')
+      ->create([
+        'uri' => 'public://example1.png',
+        'status' => 1,
+      ]);
+    $this->image->save();
+
+    $this->invalidImage = $this->container->get('entity_type.manager')
+      ->getStorage('file')
+      ->create([
+        'uri' => 'public://nonexistentimage.jpg',
+        'filename' => 'nonexistentimage.jpg',
+        'status' => 1,
+      ]);
+    $this->invalidImage->save();
+
+    $this->entityEmbedDisplayManager = $this->container->get('plugin.manager.entity_embed.display');
+  }
+
+  /**
+   * @covers ::getDefinitionsForContexts
+   */
+  public function testGetDefinitionsForContexts() {
+    $options = $this->entityEmbedDisplayManager
+      ->getDefinitionOptionsForContext([
+        'entity' => $this->image,
+        'entity_type' => $this->image->getEntityTypeId(),
+        'embed_button' => $this->imageButton,
+      ]);
+    $expected = [
+      'image:image' => 'Image',
+    ];
+    $this->assertEquals($expected, $options);
+
+    $options = $this->entityEmbedDisplayManager
+      ->getDefinitionOptionsForContext([
+        'entity' => $this->image,
+        'entity_type' => $this->image->getEntityTypeId(),
+      ]);
+    // All available plugins for the entity type.
+    $expected = [
+      'image:image' => 'Image',
+      'entity_reference:entity_reference_entity_id' => 'Entity ID',
+      'file:file_default' => 'Generic file',
+      'entity_reference:entity_reference_label' => 'Label',
+      'file:file_table' => 'Table of files',
+      'file:file_url_plain' => 'URL to file',
+      'image:image_url' => 'URL to image',
+    ];
+    $this->assertEquals($expected, $options);
+
+    // Test that output is the same as ::getDefinitionOptionsForEntity().
+    $options = $this->entityEmbedDisplayManager
+      ->getDefinitionOptionsForEntity($this->image);
+    $this->assertEquals($expected, $options);
+
+    $options = $this->entityEmbedDisplayManager
+      ->getDefinitionOptionsForContext([
+        'entity' => $this->invalidImage,
+        'entity_type' => $this->invalidImage->getEntityTypeId(),
+        'embed_button' => $this->imageButton,
+      ]);
+    // Since the image is invalid, the `image:image` display isn't returned.
+    $this->assertEmpty($options);
+
+    $options = $this->entityEmbedDisplayManager
+      ->getDefinitionOptionsForContext([
+        'entity' => $this->invalidImage,
+        'entity_type' => $this->invalidImage->getEntityTypeId(),
+      ]);
+    // Since the image is invalid, the image display plugins aren't returned.
+    $expected = [
+      'entity_reference:entity_reference_entity_id' => 'Entity ID',
+      'file:file_default' => 'Generic file',
+      'entity_reference:entity_reference_label' => 'Label',
+      'file:file_table' => 'Table of files',
+      'file:file_url_plain' => 'URL to file',
+    ];
+    $this->assertEquals($expected, $options);
+
+    // Test that output is the same as ::getDefinitionOptionsForEntity().
+    $options = $this->entityEmbedDisplayManager
+      ->getDefinitionOptionsForEntity($this->invalidImage);
+    $this->assertEquals($expected, $options);
+  }
+
+}
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedEntityBrowserTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedEntityBrowserTest.php
similarity index 58%
rename from web/modules/entity_embed/src/Tests/EntityEmbedEntityBrowserTest.php
rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedEntityBrowserTest.php
index 60367956e2..28ee225e59 100644
--- a/web/modules/entity_embed/src/Tests/EntityEmbedEntityBrowserTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedEntityBrowserTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 use Drupal\entity_browser\Entity\EntityBrowser;
 use Drupal\embed\Entity\EmbedButton;
@@ -25,12 +25,15 @@ class EntityEmbedEntityBrowserTest extends EntityEmbedDialogTest {
    * Tests the entity browser integration.
    */
   public function testEntityEmbedEntityBrowserIntegration() {
-    $this->getEmbedDialog('custom_format', 'node');
-    $this->assertResponse(200, 'Embed dialog is accessible with custom filter format and default embed button.');
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+    // Verify embed dialog is accessible with custom filter format and
+    // default embed button.
+    $this->assertSession()->statusCodeEquals(200);
 
     // Verify that an autocomplete field is available by default.
-    $this->assertFieldByName('entity_id', '', 'Entity ID/UUID field is present.');
-    $this->assertNoText('Select entities to embed', 'Entity browser button is not present.');
+    $this->assertSession()->fieldExists('entity_id');
+    $this->assertSession()
+      ->linkNotExists('Select entities to embed', 'Entity browser button is not present.');
 
     // Set up entity browser.
     $entity_browser = EntityBrowser::create([
@@ -55,13 +58,22 @@ public function testEntityEmbedEntityBrowserIntegration() {
     $embed_button->type_settings['entity_browser'] = 'entity_embed_entity_browser_test';
     $embed_button->save();
 
-    $this->getEmbedDialog('custom_format', 'node');
-    $this->assertResponse(200, 'Embed dialog is accessible with custom filter format and default embed button.');
+    // Rebuild routes, so the route called by getEmbedDialog() exists.
+    $this->container->get('router.builder')->rebuild();
+
+    $dependencies = $embed_button->getDependencies();
+    $this->assertContains('entity_browser.browser.entity_embed_entity_browser_test', $dependencies['config']);
+
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+
+    // Verify embed dialog is accessible with custom filter format and
+    // default embed button.
+    $this->assertSession()->statusCodeEquals(200);
 
     // Verify that the autocomplete field is replaced by an entity browser
     // button.
-    $this->assertNoFieldByName('entity_id', '', 'Entity ID/UUID field is present.');
-    $this->assertText('Select entities to embed', 'Entity browser button is present.');
+    $this->assertSession()->fieldNotExists('entity_id');
+    $this->assertSession()->buttonExists('Select entities to embed');
   }
 
 }
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedHooksTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedHooksTest.php
similarity index 70%
rename from web/modules/entity_embed/src/Tests/EntityEmbedHooksTest.php
rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedHooksTest.php
index 98964e7378..99d30f9735 100644
--- a/web/modules/entity_embed/src/Tests/EntityEmbedHooksTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedHooksTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 /**
  * Tests the hooks provided by entity_embed module.
@@ -17,7 +17,7 @@ class EntityEmbedHooksTest extends EntityEmbedTestBase {
   protected $state;
 
   /**
-   *
+   * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
@@ -52,36 +52,38 @@ public function testEntityEmbedHooks() {
     // implementation and ensure it is working as designed.
     $this->state->set('entity_embed_test_entity_embed_alter', TRUE);
     $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 = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test hook_entity_embed_alter()';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertText($this->node->body->value, 'Embedded node exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
+    // Verify embedded node body exists in page.
+    $this->assertSession()->responseContains($this->node->body->value);
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
     // Ensure that embedded node's title has been replaced.
-    $this->assertText('Title set by hook_entity_embed_alter', 'Title of the embedded node is replaced by hook_entity_embed_alter()');
-    $this->assertNoText($this->node->title->value, 'Original title of the embedded node is not visible.');
+    $this->assertSession()->responseContains('Title set by hook_entity_embed_alter');
+    $this->assertSession()->responseContains('test-class-added-in-alter-hook');
+    // Verify the original title of the embedded node is not visible.
+    $this->assertSession()->responseNotContains($this->node->title->value);
     $this->state->set('entity_embed_test_entity_embed_alter', FALSE);
 
     // Enable entity_embed_test.module's hook_entity_embed_context_alter()
     // implementation and ensure it is working as designed.
     $this->state->set('entity_embed_test_entity_embed_context_alter', TRUE);
     $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 = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test hook_entity_embed_context_alter()';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
     // To ensure that 'label' plugin is used, verify that the body of the
     // embedded node is not visible and the title links to the embedded node.
-    $this->assertNoText($this->node->body->value, 'Body of the embedded node does not exists in page.');
-    $this->assertText('Title set by hook_entity_embed_context_alter', 'Title of the embedded node is replaced by hook_entity_embed_context_alter()');
-    $this->assertNoText($this->node->title->value, 'Original title of the embedded node is not visible.');
-    $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists.');
+    $this->assertSession()->responseNotContains($this->node->body->value);
+    $this->assertSession()->responseContains('Title set by hook_entity_embed_context_alter');
+    $this->assertSession()->linkByHrefExists('node/' . $this->node->id(), 0, 'Link to the embedded node exists.');
     $this->state->set('entity_embed_test_entity_embed_context_alter', FALSE);
   }
 
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedTestBase.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTestBase.php
similarity index 71%
rename from web/modules/entity_embed/src/Tests/EntityEmbedTestBase.php
rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedTestBase.php
index 4833947083..c92bcecd6b 100644
--- a/web/modules/entity_embed/src/Tests/EntityEmbedTestBase.php
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTestBase.php
@@ -1,24 +1,32 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\editor\Entity\Editor;
 use Drupal\file\Entity\File;
 use Drupal\filter\Entity\FilterFormat;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\TestFileCreationTrait;
 
 /**
  * Base class for all entity_embed tests.
  */
-abstract class EntityEmbedTestBase extends WebTestBase {
+abstract class EntityEmbedTestBase extends BrowserTestBase {
+
+  use TestFileCreationTrait;
 
   /**
    * Modules to enable.
    *
    * @var array
    */
-  public static $modules = ['entity_embed', 'entity_embed_test', 'node', 'ckeditor'];
+  protected static $modules = [
+    'entity_embed',
+    'entity_embed_test',
+    'node',
+    'ckeditor',
+  ];
 
   /**
    * The test user.
@@ -35,7 +43,7 @@ abstract class EntityEmbedTestBase extends WebTestBase {
   protected $node;
 
   /**
-   *
+   * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
@@ -48,6 +56,15 @@ protected function setUp() {
       'format' => 'custom_format',
       'name' => 'Custom format',
       'filters' => [
+        'filter_align' => [
+          'status' => 1,
+        ],
+        'filter_caption' => [
+          'status' => 1,
+        ],
+        'filter_html_image_secure' => [
+          'status' => 1,
+        ],
         'entity_embed' => [
           'status' => 1,
         ],
@@ -81,10 +98,10 @@ protected function setUp() {
     $this->drupalLogin($this->webUser);
 
     // Create a sample node to be embedded.
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Embed Test Node';
-    $settings['body'] = array('value' => 'This node is to be used for embedding in other nodes.', 'format' => 'custom_format');
+    $settings['body'] = ['value' => 'This node is to be used for embedding in other nodes.', 'format' => 'custom_format'];
     $this->node = $this->drupalCreateNode($settings);
   }
 
@@ -92,10 +109,11 @@ protected function setUp() {
    * Retrieves a sample file of the specified type.
    *
    * @return \Drupal\file\FileInterface
+   *   The test file created.
    */
   protected function getTestFile($type_name, $size = NULL) {
     // Get a file to upload.
-    $file = current($this->drupalGetTestFiles($type_name, $size));
+    $file = current($this->getTestFiles($type_name, $size));
 
     // Add a filesize property to files as would be read by
     // \Drupal\file\Entity\File::load().
@@ -107,15 +125,12 @@ protected function getTestFile($type_name, $size = NULL) {
   }
 
   /**
-   *
+   * Assert that the expected display plugins are available for the entity.
    */
   public function assertAvailableDisplayPlugins(EntityInterface $entity, array $expected_plugins, $message = '') {
     $plugin_options = $this->container->get('plugin.manager.entity_embed.display')
       ->getDefinitionOptionsForEntity($entity);
-    // @todo Remove the sorting so we can actually test return order.
-    ksort($plugin_options);
-    sort($expected_plugins);
-    $this->assertEqual(array_keys($plugin_options), $expected_plugins, $message);
+    $this->assertEquals([], array_diff($expected_plugins, array_keys($plugin_options)), $message);
   }
 
 }
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedTwigTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTwigTest.php
similarity index 74%
rename from web/modules/entity_embed/src/Tests/EntityEmbedTwigTest.php
rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedTwigTest.php
index 6ea1c13dc6..6ec214a652 100644
--- a/web/modules/entity_embed/src/Tests/EntityEmbedTwigTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedTwigTest.php
@@ -1,6 +1,8 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
+
+use Drupal\entity_embed\Twig\EntityEmbedTwigExtension;
 
 /**
  * Tests Twig extension provided by entity_embed.
@@ -10,25 +12,20 @@
 class EntityEmbedTwigTest extends EntityEmbedTestBase {
 
   /**
-   *
+   * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
-    \Drupal::service('theme_handler')->install(array('test_theme'));
+    \Drupal::service('theme_installer')->install(['test_theme']);
   }
 
   /**
    * Tests that the provided Twig extension loads the service appropriately.
    */
   public function testTwigExtensionLoaded() {
-    $twig_service = \Drupal::service('twig');
-
-    $ext = $twig_service->getExtension('entity_embed.twig.entity_embed_twig_extension');
-
-    // @todo why is the string
-    // 'Drupal\\entity_embed\\Twig\\EntityEmbedTwigExtension'
-    // and not '\Drupal\entity_embed\Twig\EntityEmbedTwigExtension' ?
-    $this->assertEqual(get_class($ext), 'Drupal\\entity_embed\\Twig\\EntityEmbedTwigExtension', 'Extension loaded successfully.');
+    $ext = $this->container->get('twig')->getExtension(EntityEmbedTwigExtension::class);
+    $this->assertNotEmpty($ext);
+    $this->assertInstanceOf(EntityEmbedTwigExtension::class, $ext, 'Extension loaded successfully.');
   }
 
   /**
diff --git a/web/modules/entity_embed/src/Tests/EntityEmbedUpdateHookTest.php b/web/modules/entity_embed/tests/src/Functional/EntityEmbedUpdateHookTest.php
similarity index 76%
rename from web/modules/entity_embed/src/Tests/EntityEmbedUpdateHookTest.php
rename to web/modules/entity_embed/tests/src/Functional/EntityEmbedUpdateHookTest.php
index e8572b26ac..afa86b3e55 100644
--- a/web/modules/entity_embed/src/Tests/EntityEmbedUpdateHookTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/EntityEmbedUpdateHookTest.php
@@ -1,8 +1,8 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
-use Drupal\system\Tests\Update\UpdatePathTestBase;
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
 
 /**
  * Tests the update hooks in entity_embed module.
@@ -16,8 +16,8 @@ class EntityEmbedUpdateHookTest extends UpdatePathTestBase {
    */
   protected function setDatabaseDumpFiles() {
     $this->databaseDumpFiles = [
-      DRUPAL_ROOT . '/core/modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
-      __DIR__ . '/../../tests/fixtures/update/entity_embed.update-hook-test.php',
+      $this->getDrupalRoot() . '/core/modules/system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz',
+      __DIR__ . '/../../fixtures/update/entity_embed.update-hook-test.php',
     ];
   }
 
@@ -40,8 +40,8 @@ protected function setUp() {
    */
   protected function doSelectionTest() {
     parent::doSelectionTest();
-    $this->assertRaw('8002 -   Updates the default mode settings.');
-    $this->assertRaw('8003 -   Updates allowed HTML for all filter format config entities that have an  Entity Embed button.');
+    $this->assertSession()->responseContains('8002 -   Updates the default mode settings.');
+    $this->assertSession()->responseContains('8003 -   Updates allowed HTML for all filter configs that have an Entity Embed button.');
   }
 
   /**
@@ -58,7 +58,7 @@ public function todotestPostUpdate() {
   /**
    * Tests entity_embed_update_8003().
    */
-  public function testAllowedHTML() {
+  public function testAllowedAttributes() {
     $allowed_html = '<drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-settings data-align data-caption data-embed-button>';
     $expected_allowed_html = '<drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-settings data-entity-embed-display-settings data-align data-caption data-embed-button>';
     $filter_format = $this->container->get('entity_type.manager')->getStorage('filter_format')->load('full_html');
@@ -74,7 +74,7 @@ public function testAllowedHTML() {
     $this->runUpdates();
     $filter_format = $this->container->get('entity_type.manager')->getStorage('filter_format')->load('full_html');
     $filter_html = $filter_format->filters('filter_html');
-    $this->assertEqual($expected_allowed_html, $filter_html->getConfiguration()['settings']['allowed_html'], 'Allowed html is correct');
+    $this->assertEquals($expected_allowed_html, $filter_html->getConfiguration()['settings']['allowed_html'], 'Allowed html is correct');
   }
 
 }
diff --git a/web/modules/entity_embed/src/Tests/EntityReferenceFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/EntityReferenceFieldFormatterTest.php
similarity index 68%
rename from web/modules/entity_embed/src/Tests/EntityReferenceFieldFormatterTest.php
rename to web/modules/entity_embed/tests/src/Functional/EntityReferenceFieldFormatterTest.php
index 916578798a..397b2ecaba 100644
--- a/web/modules/entity_embed/src/Tests/EntityReferenceFieldFormatterTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/EntityReferenceFieldFormatterTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 use Drupal\Core\Form\FormState;
 
@@ -19,17 +19,19 @@ class EntityReferenceFieldFormatterTest extends EntityEmbedTestBase {
   protected $menu;
 
   /**
-   *
+   * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
 
     // Add a new menu entity which does not has a view controller.
-    $this->menu = entity_create('menu', array(
-      'id' => 'menu_name',
-      'label' => 'Label',
-      'description' => 'Description text',
-    ));
+    $this->menu = \Drupal::entityTypeManager()
+      ->getStorage('menu')
+      ->create([
+        'id' => 'menu_name',
+        'label' => 'Label',
+        'description' => 'Description text',
+      ]);
     $this->menu->save();
   }
 
@@ -60,33 +62,33 @@ public function testEntityReferenceFieldFormatter() {
 
     // Ensure that correct form attributes are returned for
     // 'entity_reference:entity_reference_entity_id' plugin.
-    $form = array();
+    $form = [];
     $form_state = new FormState();
-    $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_id', array());
+    $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_id', []);
     $display->setContextValue('entity', $this->node);
     $conf_form = $display->buildConfigurationForm($form, $form_state);
-    $this->assertIdentical(array_keys($conf_form), array());
+    $this->assertSame([], array_keys($conf_form));
 
     // Ensure that correct form attributes are returned for
     // 'entity_reference:entity_reference_entity_view' plugin.
-    $form = array();
+    $form = [];
     $form_state = new FormState();
-    $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_view', array());
+    $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_entity_view', []);
     $display->setContextValue('entity', $this->node);
     $conf_form = $display->buildConfigurationForm($form, $form_state);
-    $this->assertIdentical($conf_form['view_mode']['#type'], 'select');
-    $this->assertIdentical((string) $conf_form['view_mode']['#title'], 'View mode');
+    $this->assertSame('select', $conf_form['view_mode']['#type']);
+    $this->assertSame('View mode', (string) $conf_form['view_mode']['#title']);
 
     // Ensure that correct form attributes are returned for
     // 'entity_reference:entity_reference_label' plugin.
-    $form = array();
+    $form = [];
     $form_state = new FormState();
-    $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_label', array());
+    $display = $this->container->get('plugin.manager.entity_embed.display')->createInstance('entity_reference:entity_reference_label', []);
     $display->setContextValue('entity', $this->node);
     $conf_form = $display->buildConfigurationForm($form, $form_state);
-    $this->assertIdentical(array_keys($conf_form), array('link'));
-    $this->assertIdentical($conf_form['link']['#type'], 'checkbox');
-    $this->assertIdentical((string) $conf_form['link']['#title'], 'Link label to the referenced entity');
+    $this->assertSame(['link'], array_keys($conf_form));
+    $this->assertSame('checkbox', $conf_form['link']['#type']);
+    $this->assertSame('Link label to the referenced entity', (string) $conf_form['link']['#title']);
 
     // Ensure that 'Rendered Entity' plugin is not available for an entity not
     // having a view controller.
@@ -100,41 +102,43 @@ public function testEntityReferenceFieldFormatter() {
   public function testFilterEntityReferencePlugins() {
     // Test 'Label' Entity Embed Display plugin.
     $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-display-settings=\'{"link":1}\'>This placeholder should not be rendered.</drupal-entity>';
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test entity_reference:entity_reference_label Entity Embed Display plugin';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertText($this->node->title->value, 'Title of the embedded node exists in page.');
-    $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
-    $this->assertLinkByHref('node/' . $this->node->id(), 0, 'Link to the embedded node exists.');
+    // Verify title of embedded node exists in page.
+    $this->assertSession()->responseContains($this->node->title->value);
+    // Verify body of embedded node does not exists in page.
+    $this->assertSession()->responseNotContains($this->node->body->value);
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
+    $this->assertSession()->linkByHrefExists('node/' . $this->node->id(), 0, 'Link to the embedded node exists.');
 
     // Test 'Entity ID' Entity Embed Display plugin.
     $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_entity_id">This placeholder should not be rendered.</drupal-entity>';
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test entity_reference:entity_reference_entity_id Entity Embed Display plugin';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertText($this->node->id(), 'ID of the embedded node exists in page.');
-    $this->assertNoText($this->node->title->value, 'Title of the embedded node does not exists in page.');
-    $this->assertNoText($this->node->body->value, 'Body of embedded node does not exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
-    $this->assertNoLinkByHref('node/' . $this->node->id(), 'Link to the embedded node does not exists.');
+    $this->assertSession()->responseContains($this->node->id());
+    $this->assertSession()->responseNotContains($this->node->title->value);
+    $this->assertSession()->responseNotContains($this->node->body->value);
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
+    $this->assertSession()->linkByHrefNotExists('node/' . $this->node->id(), 'Link to the embedded node does not exists.');
 
     // Test 'Rendered entity' Entity Embed Display plugin.
     $content = '<drupal-entity 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=\'{"view_mode":"teaser"}\'>This placeholder should not be rendered.</drupal-entity>';
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test entity_reference:entity_reference_label Entity Embed Display plugin';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertText($this->node->body->value, 'Body of embedded node does not exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
+    $this->assertSession()->responseContains($this->node->body->value, 'Body of embedded node does not exists in page.');
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
   }
 
 }
diff --git a/web/modules/entity_embed/src/Tests/FileFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/FileFieldFormatterTest.php
similarity index 70%
rename from web/modules/entity_embed/src/Tests/FileFieldFormatterTest.php
rename to web/modules/entity_embed/tests/src/Functional/FileFieldFormatterTest.php
index ba566c6b68..07b78da5b5 100644
--- a/web/modules/entity_embed/src/Tests/FileFieldFormatterTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/FileFieldFormatterTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Form\FormState;
@@ -27,7 +27,7 @@ class FileFieldFormatterTest extends EntityEmbedTestBase {
   protected $file;
 
   /**
-   *
+   * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
@@ -49,36 +49,39 @@ public function testFileFieldFormatter() {
 
     // Ensure that correct form attributes are returned for the file field
     // formatter plugins.
-    $form = array();
+    $form = [];
     $form_state = new FormState();
-    $plugins = array(
+    $plugins = [
       'file:file_table',
       'file:file_default',
       'file:file_url_plain',
-    );
+    ];
     // Ensure that description field is available for all the 'file' plugins.
     foreach ($plugins as $plugin) {
       $display = $this->container->get('plugin.manager.entity_embed.display')
         ->createInstance($plugin, []);
       $display->setContextValue('entity', $this->file);
       $conf_form = $display->buildConfigurationForm($form, $form_state);
-      $this->assertIdentical(array_keys($conf_form), array('description'));
-      $this->assertIdentical($conf_form['description']['#type'], 'textfield');
-      $this->assertIdentical((string) $conf_form['description']['#title'], 'Description');
+      $this->assertArrayHasKey('description', $conf_form);
+      $this->assertSame('textfield', $conf_form['description']['#type']);
+      $this->assertSame('Description', (string) $conf_form['description']['#title']);
     }
 
     // Test entity embed using 'Generic file' Entity Embed Display plugin.
-    $embed_settings = array('description' => "This is sample description");
+    $embed_settings = [
+      'description' => 'This is sample description',
+    ];
     $content = '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" data-entity-embed-display="file:file_default" data-entity-embed-display-settings=\'' . Json::encode($embed_settings) . '\'>This placeholder should not be rendered.</drupal-entity>';
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test entity embed with file:file_default';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertText($embed_settings['description'], 'Description of the embedded file exists in page.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
-    $this->assertLinkByHref(file_create_url($this->file->getFileUri()), 0, 'Link to the embedded file exists.');
+    // Verify description of the embedded file exists in page.
+    $this->assertSession()->responseContains($embed_settings['description']);
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
+    $this->assertSession()->linkByHrefExists(file_create_url($this->file->getFileUri()), 0, 'Link to the embedded file exists.');
   }
 
 }
diff --git a/web/modules/entity_embed/src/Tests/ImageFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/ImageFieldFormatterTest.php
similarity index 69%
rename from web/modules/entity_embed/src/Tests/ImageFieldFormatterTest.php
rename to web/modules/entity_embed/tests/src/Functional/ImageFieldFormatterTest.php
index 19398c0987..d45d50624d 100644
--- a/web/modules/entity_embed/src/Tests/ImageFieldFormatterTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/ImageFieldFormatterTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Form\FormState;
@@ -34,7 +34,7 @@ class ImageFieldFormatterTest extends EntityEmbedTestBase {
   protected $file;
 
   /**
-   *
+   * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
@@ -58,50 +58,53 @@ public function testImageFieldFormatter() {
     ]);
 
     // Ensure that correct form attributes are returned for the image plugin.
-    $form = array();
+    $form = [];
     $form_state = new FormState();
     $display = $this->container->get('plugin.manager.entity_embed.display')
       ->createInstance('image:image', []);
     $display->setContextValue('entity', $this->image);
     $conf_form = $display->buildConfigurationForm($form, $form_state);
-    $this->assertIdentical(array_keys($conf_form), array(
+    $expected = [
       'image_style',
       'image_link',
       'alt',
       'title',
-    ));
-    $this->assertIdentical($conf_form['image_style']['#type'], 'select');
-    $this->assertIdentical((string) $conf_form['image_style']['#title'], 'Image style');
-    $this->assertIdentical($conf_form['image_link']['#type'], 'select');
-    $this->assertIdentical((string) $conf_form['image_link']['#title'], 'Link image to');
-    $this->assertIdentical($conf_form['alt']['#type'], 'textfield');
-    $this->assertIdentical((string) $conf_form['alt']['#title'], 'Alternate text');
-    $this->assertIdentical($conf_form['title']['#type'], 'textfield');
-    $this->assertIdentical((string) $conf_form['title']['#title'], 'Title');
+    ];
+    $this->assertSame($expected, array_keys($conf_form));
+    $this->assertSame('select', $conf_form['image_style']['#type']);
+    $this->assertSame('Image style', (string) $conf_form['image_style']['#title']);
+    $this->assertSame('select', $conf_form['image_link']['#type']);
+    $this->assertSame('Link image to', (string) $conf_form['image_link']['#title']);
+    $this->assertSame('textfield', $conf_form['alt']['#type']);
+    $this->assertSame('Alternate text', (string) $conf_form['alt']['#title']);
+    $this->assertSame('textfield', $conf_form['title']['#type']);
+    $this->assertSame('Title', (string) $conf_form['title']['#title']);
 
     // Test entity embed using 'Image' Entity Embed Display plugin.
     $alt_text = "This is sample description";
     $title = "This is sample title";
-    $embed_settings = array('image_link' => 'file');
+    $embed_settings = ['image_link' => 'file'];
     $content = '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->image->uuid() . '" data-entity-embed-display="image:image" data-entity-embed-display-settings=\'' . Json::encode($embed_settings) . '\' alt="' . $alt_text . '" title="' . $title . '">This placeholder should not be rendered.</drupal-entity>';
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test entity embed with image:image';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
-    $this->assertRaw($alt_text, 'Alternate text for the embedded image is visible when embed is successful.');
-    $this->assertNoText(strip_tags($content), 'Placeholder does not appears in the output when embed is successful.');
-    $this->assertLinkByHref(file_create_url($this->image->getFileUri()), 0, 'Link to the embedded image exists.');
+    // Verify alternate text for the embedded image is visible
+    // when embed is successful.
+    $this->assertSession()->responseContains($alt_text);
+    $this->assertSession()->responseNotContains('This placeholder should not be rendered.');
+    $this->assertSession()->linkByHrefExists(file_create_url($this->image->getFileUri()), 0, 'Link to the embedded image exists.');
 
     // Embed all three field types in one, to ensure they all render correctly.
     $content = '<drupal-entity data-entity-type="node" data-entity-uuid="' . $this->node->uuid() . '" data-entity-embed-display="entity_reference:entity_reference_label"></drupal-entity>';
     $content .= '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" data-entity-embed-display="file:file_default"></drupal-entity>';
     $content .= '<drupal-entity data-entity-type="file" data-entity-uuid="' . $this->image->uuid() . '" data-entity-embed-display="image:image"></drupal-entity>';
-    $settings = array();
+    $settings = [];
     $settings['type'] = 'page';
     $settings['title'] = 'Test node entity embedded first then a file entity';
-    $settings['body'] = array(array('value' => $content, 'format' => 'custom_format'));
+    $settings['body'] = [['value' => $content, 'format' => 'custom_format']];
     $node = $this->drupalCreateNode($settings);
     $this->drupalGet('node/' . $node->id());
   }
diff --git a/web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php b/web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php
new file mode 100644
index 0000000000..d72290138b
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Functional/RecursionProtectionTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Functional;
+
+use Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter;
+
+/**
+ * Tests recursive rendering protection.
+ *
+ * @group entity_embed
+ *
+ * @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTest::testRecursionProtection
+ */
+class RecursionProtectionTest extends EntityEmbedTestBase {
+
+  /**
+   * Tests self embedding.
+   */
+  public function testSelfEmbedding() {
+    $node = $this->drupalCreateNode([
+      'type' => 'article',
+      'title' => "Pirate Chinchilla LLama",
+      'body' => [
+        'value' => 'temp',
+        'format' => 'custom_format',
+      ],
+    ]);
+    $node->save();
+    $content = '<div class="pirate">Ahoy, Matey!</div> <drupal-entity data-entity-type="node" data-entity-uuid="' . $node->uuid() . '" data-entity-embed-display="view_mode:node.full"></drupal-entity>';
+    $node->set('body', [
+      'value' => $content,
+      'format' => 'custom_format',
+    ]);
+    $node->save();
+    $this->drupalGet('node/' . $node->id());
+    $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT + 1, $this->getSession()->getPage()->findAll('xpath', '//div[@class="pirate"]'));
+  }
+
+  /**
+   * Tests circular embedding.
+   */
+  public function testCircularEmbedding() {
+    $node1 = $this->drupalCreateNode([
+      'type' => 'article',
+      'title' => "Grandpa",
+      'body' => [
+        'value' => 'temp',
+        'format' => 'custom_format',
+      ],
+    ]);
+    $node1->save();
+    $node2 = $this->drupalCreateNode([
+      'type' => 'article',
+      'title' => "Son",
+      'body' => [
+        'value' => 'temp',
+        'format' => 'custom_format',
+      ],
+    ]);
+    $node2->save();
+    $content = '<div class="node-2-embed">Embedded Son</div> <drupal-entity data-entity-type="node" data-entity-uuid="' . $node2->uuid() . '" data-view-mode="full"></drupal-entity>';
+    $node1->set('body', [
+      'value' => $content,
+      'format' => 'custom_format',
+    ]);
+    $node1->save();
+    $content = '<div class="node-1-embed">Embedded Son who is own grandpa</div> <drupal-entity data-entity-type="node" data-entity-uuid="' . $node1->uuid() . '" data-view-mode="full"></drupal-entity>';
+    $node2->set('body', [
+      'value' => $content,
+      'format' => 'custom_format',
+    ]);
+    $node2->save();
+    $this->drupalGet('node/' . $node1->id());
+    $page = $this->getSession()->getPage();
+    $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT, $page->findAll('xpath', '//div[@class="node-1-embed"]'));
+    $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT + 1, $page->findAll('xpath', '//div[@class="node-2-embed"]'));
+
+    $this->drupalGet('node/' . $node2->id());
+    $page = $this->getSession()->getPage();
+    $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT + 1, $page->findAll('xpath', '//div[@class="node-1-embed"]'));
+    $this->assertCount(EntityEmbedFilter::RECURSIVE_RENDER_LIMIT, $page->findAll('xpath', '//div[@class="node-2-embed"]'));
+  }
+
+}
diff --git a/web/modules/entity_embed/src/Tests/ViewModeFieldFormatterTest.php b/web/modules/entity_embed/tests/src/Functional/ViewModeFieldFormatterTest.php
similarity index 59%
rename from web/modules/entity_embed/src/Tests/ViewModeFieldFormatterTest.php
rename to web/modules/entity_embed/tests/src/Functional/ViewModeFieldFormatterTest.php
index 7712da9fc2..cab2e3f44f 100644
--- a/web/modules/entity_embed/src/Tests/ViewModeFieldFormatterTest.php
+++ b/web/modules/entity_embed/tests/src/Functional/ViewModeFieldFormatterTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\entity_embed\Tests;
+namespace Drupal\Tests\entity_embed\Functional;
 
 use Drupal\Core\Form\FormState;
 
@@ -31,7 +31,7 @@ public function testViewModeFieldFormatter() {
         ->createInstance($plugin, []);
       $display->setContextValue('entity', $this->node);
       $conf_form = $display->buildConfigurationForm($form, $form_state);
-      $this->assertIdentical(array_keys($conf_form), []);
+      $this->assertSame([], array_keys($conf_form));
     }
   }
 
@@ -49,8 +49,32 @@ public function testFilterViewModePlugins() {
       $this->drupalGet('node/' . $node->id());
       $plugin = explode('.', $plugin);
       $view_mode = str_replace('_', '-', end($plugin));
-      $this->assertRaw('node--view-mode-' . $view_mode, 'Node rendered in the correct view mode: ' . $view_mode . '.');
+      $this->assertSession()->responseContains('node--view-mode-' . $view_mode, 'Node rendered in the correct view mode: ' . $view_mode . '.');
     }
   }
 
+  /**
+   * Tests dependencies on EntityViewMode config entities.
+   */
+  public function testViewModeDependencies() {
+    $button = $this->container
+      ->get('entity_type.manager')
+      ->getStorage('embed_button')
+      ->load('node');
+
+    $config = $button->get('type_settings');
+    $config['display_plugins'] = ['view_mode:node.teaser'];
+    $button->set('type_settings', $config);
+    $button->save();
+    $dependencies = $button->getDependencies();
+    $this->assertContains('core.entity_view_mode.node.teaser', $dependencies['config']);
+
+    // Test that removing teaser view mode removes the dependency.
+    $config['display_plugins'] = ['view_mode:node.full'];
+    $button->set('type_settings', $config);
+    $button->save();
+    $dependencies = $button->getDependencies();
+    $this->assertNotContains('core.entity_view_mode.node.teaser', $dependencies['config']);
+  }
+
 }
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php
new file mode 100644
index 0000000000..4ed8503e86
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ButtonAdminTest.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+
+/**
+ * Tests the creation and configuration of entity embed buttons.
+ *
+ * @group entity_embed
+ */
+class ButtonAdminTest extends WebDriverTestBase {
+
+  use MediaTypeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'node',
+    'user',
+    'media',
+    'entity_embed',
+  ];
+
+  /**
+   * The user to use during testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->container
+      ->get('entity_type.manager')
+      ->getStorage('entity_view_mode')
+      ->create([
+        'id' => 'media.thumb',
+        'targetEntityType' => 'media',
+      ])
+      ->save();
+
+    $this->createContentType([
+      'type' => 'article',
+      'label' => 'Article',
+    ]);
+
+    $this->createMediaType('image', [
+      'id' => 'image',
+      'label' => 'Image',
+    ]);
+
+    $this->adminUser = $this->drupalCreateUser([
+      'administer embed buttons',
+    ]);
+
+    // Delete the existing node button provided by entity_embed module, so that
+    // we can create a button with the same machine name.
+    $this->container->get('entity_type.manager')
+      ->getStorage('embed_button')
+      ->load('node')
+      ->delete();
+  }
+
+  /**
+   * Tests the entity embed button administration functionality.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID as well as the label and machine name of the button.
+   * @param string $bundle_id
+   *   The bundle to select, if provided.
+   * @param string $entity_embed_display_plugin_id
+   *   The entity embed display plugin ID to select on the form.
+   *
+   * @dataProvider embedButtonAdminProvider
+   */
+  public function testEmbedButtonAdmin($entity_type_id, $bundle_id, $entity_embed_display_plugin_id) {
+    $this->drupalLogin($this->adminUser);
+
+    $this->drupalGet('admin/config/content/embed/button/add');
+
+    $page = $this->getSession()->getPage();
+    $page->fillField('label', $entity_type_id);
+    $page->selectFieldOption('type_id', 'entity');
+    $page->waitFor(10, function () use ($page) {
+      return $page->hasField('type_settings[entity_type]');
+    });
+    $page->selectFieldOption('type_settings[entity_type]', $entity_type_id);
+    $page->waitFor(10, function () use ($page, $bundle_id) {
+      return $page->hasField('type_settings[bundles][' . $bundle_id . ']');
+    });
+    $page->checkField('type_settings[display_plugins][' . $entity_embed_display_plugin_id . ']');
+    $page->pressButton('Save');
+
+    $this->assertContains('The embed button ' . $entity_type_id . ' has been added.', $page->getText());
+    $this->assertSession()->linkByHrefExists('/admin/config/content/embed/button/manage/' . $entity_type_id);
+
+    $this->drupalGet('/admin/config/content/embed/button/manage/' . $entity_type_id);
+
+    $page->findField('type_id')->hasAttribute('disabled');
+    $page->findField('type_settings[entity_type]')->hasAttribute('disabled');
+  }
+
+  /**
+   * Data provider for ::testEmbedButtonAdmin().
+   */
+  public function embedButtonAdminProvider() {
+    return [
+      'article nodes embedded using teaser view mode' => [
+        'node',
+        'article',
+        'view_mode:node.teaser',
+      ],
+      'users embedded using full view mode' => [
+        'user',
+        NULL,
+        'view_mode:user.full',
+      ],
+      'image media items embedded using thumb view mode' => [
+        'media',
+        'image',
+        'view_mode:media.thumb',
+      ],
+      'files embedded using plain URL' => [
+        'file',
+        NULL,
+        'file:file_url_plain',
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
new file mode 100644
index 0000000000..c4d97df4f4
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
@@ -0,0 +1,303 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+
+/**
+ * Tests ckeditor integration.
+ *
+ * @group entity_embed
+ */
+class CKEditorIntegrationTest extends EntityEmbedTestBase {
+
+  use SortableTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'field_ui',
+    'node',
+    'ckeditor',
+    'views',
+    'entity_embed',
+    'entity_embed_test',
+  ];
+
+  /**
+   * The test button.
+   *
+   * @var Drupal\embed\Entity\EmbedButton
+   */
+  protected $button;
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * A test node to be used for embedding.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->button = $this->container->get('entity_type.manager')
+      ->getStorage('embed_button')
+      ->load('node');
+    $settings = $this->button->getTypeSettings();
+    $settings['display_plugins'] = [
+      'entity_reference:entity_reference_label',
+    ];
+    $this->button->set('type_settings', $settings);
+    $this->button->save();
+
+    $format = FilterFormat::create([
+      'format' => 'embed_test',
+      'name' => 'Embed format',
+      'filters' => [],
+    ]);
+    $format->save();
+
+    Editor::create([
+      'format' => 'embed_test',
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [
+            [
+              [
+                'name' => 'Tools',
+                'items' => [
+                  'Source',
+                  'Undo',
+                  'Redo',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+    ])->save();
+
+    // Create a page content type.
+    $this->drupalCreateContentType([
+      'type' => 'page',
+      'name' => 'Basic page',
+    ]);
+
+    $this->adminUser = $this->drupalCreateUser([
+      'access administration pages',
+      'administer filters',
+      'administer display modes',
+      'administer embed buttons',
+      'administer site configuration',
+      'administer display modes',
+      'administer content types',
+      'administer node display',
+      'access content',
+      'create page content',
+      'edit own page content',
+      $format->getPermissionName(),
+    ]);
+
+    $this->drupalLogin($this->adminUser);
+
+    // Create a sample node.
+    $this->drupalCreateNode([
+      'type' => 'page',
+      'title' => 'Billy Bones',
+      'body' => [
+        'value' => 'He lacks two fingers.',
+      ],
+    ]);
+
+    $this->drupalCreateNode([
+      'type' => 'page',
+      'title' => 'Long John Silver',
+      'body' => [
+        'value' => 'A one-legged seafaring man.',
+      ],
+    ]);
+  }
+
+  /**
+   * Test integration with Filter, Editor and Ckeditor.
+   */
+  public function testIntegration() {
+    $this->drupalGet('admin/config/content/formats/manage/embed_test');
+
+    $page = $this->getSession()->getPage();
+
+    $page->checkField('filters[entity_embed][status]');
+    $page->checkField('filters[filter_html][status]');
+
+    // Add "Embeds" toolbar button group to the active toolbar.
+    $this->assertSession()->buttonExists('Show group names')->press();
+    $this->assertSession()->waitForElementVisible('css', '.ckeditor-add-new-group');
+    $this->assertSession()->buttonExists('Add group')->press();
+    $this->assertSession()->waitForElementVisible('css', '[name="group-name"]')->setValue('Embeds');
+    $this->assertSession()->buttonExists('Apply')->press();
+
+    // Verify the <drupal-entity> tag is not yet allowed.
+    $allowed_html = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]')->getValue();
+    $this->assertNotContains('drupal-entity', $allowed_html);
+
+    // Verify that after dragging the Entity Embed CKEditor plugin button into
+    // the active toolbar, the <drupal-entity> tag is allowed, as well as some
+    // attributes.
+    $item = 'li[data-drupal-ckeditor-button-name="' . $this->button->id() . '"]';
+    $from = "ul $item";
+    $target = 'ul.ckeditor-toolbar-group-buttons';
+
+    $this->assertSession()->waitForElementVisible('css', $target);
+    $this->sortableTo($item, $from, $target);
+    $allowed_html_updated = $this->assertSession()
+      ->fieldExists('filters[filter_html][settings][allowed_html]')
+      ->getValue();
+    $this->assertContains('drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button', $allowed_html_updated);
+
+    $this->assertSession()->buttonExists('Save configuration')->press();
+    $this->assertSession()->responseContains('The text format <em class="placeholder">Embed format</em> has been updated.');
+    $filterFormat = $this->container->get('entity_type.manager')
+      ->getStorage('filter_format')
+      ->load('embed_test');
+
+    $settings = $filterFormat->filters('filter_html')->settings;
+    $allowed_html = $settings['allowed_html'];
+
+    $this->assertContains('drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button', $allowed_html);
+
+    // Verify that the Entity Embed button shows up and results in an
+    // operational entity embedding experience in the text editor.
+    $this->drupalGet('/node/add/page');
+    $this->waitForEditor();
+    $this->assertSame(1, $this->getCkeditorUndoSnapshotCount());
+    $this->getSession()->executeScript("CKEDITOR.instances['edit-body-0-value'].setData('<p>Goodbye world!</p>');");
+    $this->assertSame(2, $this->getCkeditorUndoSnapshotCount());
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->responseContains('entity_embed.editor.css');
+    $this->assertSession()->responseContains('hidden.module.css');
+    $this->assertSession()->pageTextNotContains('Billy Bones');
+    $this->pressEditorButton($this->button->id());
+    $this->assertSession()->waitForId('drupal-modal');
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Billy Bones (1)');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->responseContains('Selected entity');
+    $this->assertSession()->responseContains('Billy Bones');
+    $this->assertSession()->elementExists('css', 'button.button--primary')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    // Verify that the embedded entity gets a preview inside the text editor.
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->pageTextContains('Billy Bones');
+    $this->getSession()->switchToIFrame();
+    $this->assertSame(3, $this->getCkeditorUndoSnapshotCount());
+    $this->getSession()
+      ->getPage()
+      ->find('css', 'input[name="title[0][value]"]')
+      ->setValue('Pirates');
+    // Verify that undo/redo work.
+    $this->assertCkeditorUndoOrRedo('undo', ['Goodbye world!'], ['Billy Bones']);
+    $this->assertCkeditorUndoOrRedo('undo', [], ['Billy Bones', 'Goodbye world!']);
+    $this->assertCkeditorUndoOrRedo('redo', ['Goodbye world!'], ['Billy Bones']);
+    $this->assertCkeditorUndoOrRedo('redo', ['Billy Bones', 'Goodbye world!'], []);
+    // Verify that the embedded entity is rendered by the filter for end users.
+    $this->assertSession()->buttonExists('Save')->press();
+    $this->assertSession()->responseContains('Billy Bones');
+
+    $this->drupalGet('/node/3/edit');
+    $this->assignNameToCkeditorIframe();
+
+    // Verify that the text editor previews the current embedded entity.
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForText('Billy Bones');
+    $this->getSession()->switchToIFrame();
+
+    // Test opening the dialog and switching embedded nodes.
+    $this->reopenDialog();
+
+    $this->assertSession()
+      ->waitForElementVisible('css', 'div.ui-dialog-buttonset')
+      ->findButton('Back')
+      ->click();
+
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Long John Silver (2)');
+
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->responseContains('Selected entity');
+    $this->assertSession()->responseContains('Long John Silver');
+    $this->assertSession()->elementExists('css', 'button.button--primary')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Verify that the text editor previews the updated embedded entity.
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForText('Long John Silver');
+    $this->getSession()->switchToIFrame();
+    $this->assertSession()->buttonExists('Save')->press();
+    // Verify that the embedded entity is rendered by the filter for end users.
+    $this->assertSession()->responseContains('Long John Silver');
+  }
+
+  /**
+   * Asserts the consequences of CKEditor undo/redo actions.
+   *
+   * @param string $action
+   *   Either 'undo' or 'redo'.
+   * @param array $contains
+   *   The strings the CKEditor instance is expected to contain.
+   * @param array $not_contains
+   *   The strings the CKEditor instance is expected to not contain.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   * @throws \Behat\Mink\Exception\ResponseTextException
+   */
+  protected function assertCkeditorUndoOrRedo($action, array $contains, array $not_contains) {
+    if ($action !== 'undo' && $action !== 'redo') {
+      throw new \LogicException();
+    }
+    $this->pressEditorButton($action);
+    $this->getSession()->switchToIFrame('ckeditor');
+    foreach ($contains as $string) {
+      $this->assertSession()->pageTextContains($string);
+    }
+    foreach ($not_contains as $string) {
+      $this->assertSession()->pageTextNotContains($string);
+    }
+    $this->getSession()->switchToIFrame();
+  }
+
+  /**
+   * Get a CKEditor instance's undo snapshot count.
+   *
+   * @param string $instance_id
+   *   The CKEditor instance ID.
+   *
+   * @return int
+   *   The undo snapshot count.
+   */
+  protected function getCkeditorUndoSnapshotCount($instance_id = 'edit-body-0-value') {
+    $this->waitForEditor($instance_id);
+    return $this->getSession()->evaluateScript("CKEDITOR.instances['$instance_id'].undoManager.snapshots.length");
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php
new file mode 100644
index 0000000000..319863fbfe
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ConfigurationUiTest.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+
+/**
+ * Tests text format & text editor configuration UI validation.
+ *
+ * @group entity_embed
+ */
+class ConfigurationUiTest extends EntityEmbedTestBase {
+
+  use SortableTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ckeditor',
+    'entity_embed',
+  ];
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $format = FilterFormat::create([
+      'format' => 'embed_test',
+      'name' => 'Embed format',
+      'filters' => [],
+    ]);
+    $format->save();
+
+    Editor::create([
+      'format' => $format->id(),
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [
+            [
+              [
+                'name' => 'Tools',
+                'items' => [
+                  'Source',
+                  'Undo',
+                  'Redo',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+    ])->save();
+
+    $this->adminUser = $this->drupalCreateUser([
+      'administer filters',
+      $format->getPermissionName(),
+    ]);
+
+    $this->drupalLogin($this->adminUser);
+  }
+
+  /**
+   * Test integration with Filter and Text Editor form validation.
+   *
+   * @param bool $filter_html_status
+   *   Whether to enable filter_html.
+   * @param bool $entity_embed_status
+   *   Whether to enabled entity_embed.
+   * @param string|false $allowed_html
+   *   The allowed HTML to set.  Set to 'default' to test 'drupal-entity' is
+   *   missing or FALSE to leave the properly alone.
+   * @param string $expected_error_message
+   *   The error message that should display.
+   *
+   * @dataProvider providerTestValidations
+   * @dataProvider providerTestValidationWhenAdding
+   */
+  public function testValidationWhenAdding($filter_html_status, $entity_embed_status, $allowed_html, $expected_error_message) {
+    $this->drupalGet('admin/config/content/formats/add');
+
+    // Enable the `filter_html` and `entity_embed` filters, and select CKEditor
+    // as the text editor.
+    $page = $this->getSession()->getPage();
+    $page->fillField('name', 'Test Format');
+    $this->showHiddenFields();
+    $page->findField('format')->setValue('test_format');
+
+    if ($filter_html_status) {
+      $page->checkField('filters[filter_html][status]');
+    }
+    if ($entity_embed_status) {
+      $page->checkField('filters[entity_embed][status]');
+    }
+    $page->selectFieldOption('editor[editor]', 'ckeditor');
+
+    // Verify that after dragging the Entity Embed CKEditor plugin button into
+    // the active toolbar, the <drupal-entity> tag is allowed, as well as some
+    // attributes.
+    $item = 'li[data-drupal-ckeditor-button-name="test_media_entity_embed"]';
+    $from = "ul $item";
+    $target = 'ul.ckeditor-toolbar-group-buttons';
+
+    $this->assertSession()->waitForElementVisible('css', $target);
+    $this->sortableTo($item, $from, $target);
+
+    if ($allowed_html == 'default' && $entity_embed_status) {
+      // Unfortunately the <drupal-entity> tag is not yet allowed due to
+      // https://www.drupal.org/project/drupal/issues/2763075.
+      $allowed_html = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]')->getValue();
+      $this->assertNotContains('drupal-entity', $allowed_html);
+    }
+    elseif (!empty($allowed_html)) {
+      $page->fillField('filters[filter_html][settings][allowed_html]', $allowed_html);
+    }
+
+    $this->assertSession()->buttonExists('Save configuration')->press();
+
+    if ($expected_error_message) {
+      $this->assertSession()->pageTextNotContains('Added text format Test Format.');
+      $this->assertSession()->pageTextContains($expected_error_message);
+    }
+    else {
+      $this->assertSession()->pageTextContains('Added text format Test Format.');
+    }
+  }
+
+  /**
+   * Data provider for testValidationWhenAdding().
+   */
+  public function providerTestValidationWhenAdding() {
+    return [
+      'Tests validation when drupal-entity not added.' => [
+        'filters[filter_html][status]' => TRUE,
+        'filters[entity_embed][status]' => TRUE,
+        'allowed_html' => 'default',
+        'expected_error_message' => 'The Media Entity Embed button requires <drupal-entity> among the allowed HTML tags.',
+      ],
+    ];
+  }
+
+  /**
+   * Test integration with Filter and Text Editor form validation.
+   *
+   * @param bool $filter_html_status
+   *   Whether to enable filter_html.
+   * @param bool $entity_embed_status
+   *   Whether to enabled entity_embed.
+   * @param string $allowed_html
+   *   The allowed HTML to set.  Set to 'default' to test 'drupal-entity' is
+   *   present or FALSE to leave the properly alone.
+   * @param string $expected_error_message
+   *   The error message that should display.
+   *
+   * @dataProvider providerTestValidations
+   * @dataProvider providerTestValidationWhenEditing
+   */
+  public function testValidationWhenEditing($filter_html_status, $entity_embed_status, $allowed_html, $expected_error_message) {
+    $this->drupalGet('admin/config/content/formats/manage/embed_test');
+
+    // Enable the `filter_html` and `entity_embed` filters, and select CKEditor
+    // as the text editor.
+    $page = $this->getSession()->getPage();
+
+    if ($filter_html_status) {
+      $page->checkField('filters[filter_html][status]');
+    }
+    if ($entity_embed_status) {
+      $page->checkField('filters[entity_embed][status]');
+    }
+    $page->selectFieldOption('editor[editor]', 'ckeditor');
+
+    // Verify that after dragging the Entity Embed CKEditor plugin button into
+    // the active toolbar, the <drupal-entity> tag is allowed, as well as some
+    // attributes.
+    $item = 'li[data-drupal-ckeditor-button-name="test_media_entity_embed"]';
+    $from = "ul $item";
+    $target = 'ul.ckeditor-toolbar-group-buttons';
+
+    $this->assertSession()->waitForElementVisible('css', $target);
+    $this->sortableTo($item, $from, $target);
+
+    if ($allowed_html == 'default' && $entity_embed_status) {
+      $allowed_html = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]')->getValue();
+      $this->assertContains('drupal-entity', $allowed_html);
+    }
+    elseif (!empty($allowed_html)) {
+      $page->fillField('filters[filter_html][settings][allowed_html]', $allowed_html);
+    }
+
+    $this->assertSession()->buttonExists('Save configuration')->press();
+
+    if ($expected_error_message) {
+      $this->assertSession()->pageTextNotContains('The text format Embed format has been updated.');
+      $this->assertSession()->pageTextContains($expected_error_message);
+    }
+    else {
+      $this->assertSession()->pageTextContains('The text format Embed format has been updated.');
+    }
+  }
+
+  /**
+   * Data provider for testValidationWhenEditing().
+   */
+  public function providerTestValidationWhenEditing() {
+    return [
+      'Tests validation when drupal-entity not added.' => [
+        'filters[filter_html][status]' => TRUE,
+        'filters[entity_embed][status]' => TRUE,
+        'allowed_html' => 'default',
+        'expected_error_message' => FALSE,
+      ],
+    ];
+  }
+
+  /**
+   * Data provider for testValidationWhenAdding() and
+   * testValidationWhenEditing().
+   */
+  public function providerTestValidations() {
+    return [
+      'Tests that no filter_html occurs when filter_html not enabled.' => [
+        'filters[filter_html][status]' => FALSE,
+        'filters[entity_embed][status]' => TRUE,
+        'allowed_html' => FALSE,
+        'expected_error_message' => FALSE,
+      ],
+      'Tests validation when both filter_html and entity_embed are disabled.' => [
+        'filters[filter_html][status]' => FALSE,
+        'filters[entity_embed][status]' => FALSE,
+        'allowed_html' => FALSE,
+        'expected_error_message' => FALSE,
+      ],
+      'Tests validation when entity_embed filter not enabled and filter_html is enabled.' => [
+        'filters[filter_html][status]' => TRUE,
+        'filters[entity_embed][status]' => FALSE,
+        'allowed_html' => 'default',
+        'expected_error_message' => FALSE,
+      ],
+      'Tests validation when drupal-entity element has no attributes.' => [
+        'filters[filter_html][status]' => TRUE,
+        'filters[entity_embed][status]' => TRUE,
+        'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-entity>",
+        'expected_error_message' => 'The <drupal-entity> tag in the allowed HTML tags is missing the following attributes: data-entity-type, data-entity-uuid, data-entity-embed-display, data-entity-embed-display-settings, data-align, data-caption, data-embed-button, alt, title.',
+      ],
+      'Tests validation when drupal-entity element lacks some required attributes.' => [
+        'filters[filter_html][status]' => TRUE,
+        'filters[entity_embed][status]' => TRUE,
+        'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-embed-button data-langcode>",
+        'expected_error_message' => 'The <drupal-entity> tag in the allowed HTML tags is missing the following attributes: data-caption, alt, title.',
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php
new file mode 100644
index 0000000000..5e9749a0b9
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ContentTranslationTest.php
@@ -0,0 +1,344 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\Component\Utility\Html;
+use Drupal\file\Entity\File;
+use Drupal\media\Entity\Media;
+
+/**
+ * Test integration with content_translation.
+ *
+ * @group entity_embed
+ */
+class ContentTranslationTest extends EntityEmbedTestBase {
+
+  /**
+   * The 'translator' user to use during testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $translator;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_embed_translation_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->translator = $this->drupalCreateUser([
+      'use text format full_html',
+      'administer nodes',
+      'edit any article content',
+      'translate any entity',
+    ]);
+
+    $this->config('field.storage.node.body')
+      ->set('translatable', TRUE)
+      ->save();
+  }
+
+  /**
+   * Return autocomplete suggestions from the entity_id field.
+   *
+   * @param string $search_string
+   *   The search string.
+   *
+   * @return string
+   *   The text of the autocomplete suggestions.
+   */
+  protected function getAutocompleteSuggestions($search_string) {
+    $page = $this->getSession()->getPage();
+    $autocomplete_field = $field = $page->findField('entity_id');
+    $this->assertNotEmpty($autocomplete_field);
+    $autocomplete_field->setValue($search_string);
+    $this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
+    $this->assertSession()->waitOnAutocomplete();
+    $suggestions = $this->assertSession()
+      ->waitForElementVisible('css', '.ui-autocomplete');
+    $this->assertNotEmpty($suggestions);
+    return $suggestions->getText();
+  }
+
+  /**
+   * Tests the host entity's langcode is available in EntityEmbedDialog.
+   */
+  public function testHostEntityLangcode() {
+    $node = $this->createNode([
+      'type' => 'article',
+      'title' => 'Clark Kent',
+    ]);
+    $node_fr = $node->addTranslation('fr');
+    $node_fr->title = 'Superhomme';
+    $node_fr->save();
+
+    \Drupal::service('file_system')->copy($this->root . '/core/misc/druplicon.png', 'public://Smeagol.jpg');
+    /** @var \Drupal\file\FileInterface $file */
+    $file = File::create([
+      'uri' => 'public://Smeagol.jpg',
+      'uid' => $this->translator->id(),
+    ]);
+    $file->save();
+
+    $media = Media::create([
+      'bundle' => 'image',
+      'name' => 'Smeagol likes cheese',
+      'field_media_image' => [
+        [
+          'target_id' => $file->id(),
+          'alt' => 'Smeagol likes cheese alt',
+          'title' => 'Smeagol likes cheese title',
+        ],
+      ],
+    ]);
+    $media->save();
+
+    $media_fr = $media->addTranslation('fr');
+    $media_fr->name = "Gollum n'aime que la bague";
+    $media_fr->field_media_image->setValue([
+      [
+        'target_id' => $file->id(),
+        'alt' => "Gollum n'aime que la bague alt",
+        'title' => "Gollum n'aime que la bague title",
+      ],
+    ]);
+    $media_fr->save();
+
+    $host = $this->createNode([
+      'type' => 'article',
+      'title' => 'host',
+      'body' => [
+        'value' => '',
+        'format' => 'full_html',
+      ],
+    ]);
+    $host_fr = $host->addTranslation('fr');
+    $host_fr->title = 'host';
+    $host_fr->body->value = '';
+    $host_fr->body->format = 'full_html';
+    $host_fr->body->lang = 'fr';
+    $host_fr->save();
+
+    // Test the default language, as a baseline for comparison.
+    $this->drupalLogin($this->translator);
+    $this->drupalGet('node/' . $host->id() . '/edit');
+    $this->waitForEditor();
+    $this->pressEditorButton('test_node');
+    $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#entity-embed-dialog-form'));
+
+    // Assert autocomplete suggestions are in host entity language (en).
+    $suggestions = $this->getAutocompleteSuggestions('clar');
+    $this->assertContains('Clark Kent', $suggestions);
+
+    // Assert autocomplete does not show suggestions for translations not
+    // matching the host entity language.
+    $suggestions = $this->getAutocompleteSuggestions('super');
+    $this->assertNotContains('Superhomme', $suggestions);
+
+    // Select the suggestion matching the host entity language, and proceed to
+    // the review step.
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Clark Kent (1)');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+
+    // Assert that the review step displays the selected entity with the label
+    // in the host language.
+    $text = $form->getText();
+    $this->assertContains('Clark Kent', $text);
+    $this->assertNotContains('Superhomme', $text);
+
+    // Repeat the same test pattern, but now for a Media entity instead of Node.
+    $this->getSession()->reload();
+    $this->assertSession()
+      ->waitForElementVisible('css', 'a.cke_button__test_media_entity_embed')
+      ->click();
+    $this->assertSession()->waitForId('drupal-modal');
+
+    // Assert autocomplete suggestions are in host entity language (en).
+    $suggestions = $this->getAutocompleteSuggestions('Smeagol likes cheese');
+    $this->assertContains('Smeagol likes cheese', $suggestions);
+
+    // Assert autocomplete does not show suggestions for translations not
+    // matching the host entity language.
+    $suggestions = $this->getAutocompleteSuggestions("Gollum n'aime que la bague");
+    $this->assertNotContains("Gollum n'aime que la bague", $suggestions);
+
+    // Select the suggestion matching the host entity language, and proceed to
+    // the review step.
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Smeagol likes cheese (1)');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+
+    // Assert that the review step displays the selected entity with the label
+    // in the host language.
+    $text = $form->getText();
+    $this->assertContains('Smeagol likes cheese', $text);
+    $this->assertNotContains("Gollum n'aime que la bague", $text);
+
+    // Get translation of host entity.
+    $this->drupalGet('/fr/node/' . $host->id() . '/edit');
+    $this->waitForEditor();
+    $this->pressEditorButton('test_node');
+    $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#entity-embed-dialog-form'));
+
+    // Assert autocomplete suggestions are in host entity language (fr).
+    $suggestions = $this->getAutocompleteSuggestions('super');
+    $this->assertContains('Superhomme', $suggestions);
+
+    // Assert autocomplete does not show suggestions for translations not
+    // matching the host entity language.
+    $suggestions = $this->getAutocompleteSuggestions('clark');
+    $this->assertNotContains('Clark Kent', $suggestions);
+
+    // Select the suggestion matching the host entity language, and proceed to
+    // the review step.
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Superhomme (1)');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+
+    // Assert the translated label appears, not the original.
+    $text = $form->getText();
+    $this->assertContains('Superhomme', $text);
+    $this->assertNotContains('Clark Kent', $text);
+
+    // Choose to display as label without link.
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]')
+      ->setValue('entity_reference:entity_reference_label');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][link]')
+      ->uncheck();
+    $this->assertSession()->elementExists('css', 'button.button--primary')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Verify that the embedded entity preview in CKEditor displays the label in
+    // the correct language (fr).
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->pageTextContains('Superhomme');
+
+    // Repeat the same test pattern, but now for a Media entity instead of Node.
+    $this->getSession()->reload();
+    $this->assertSession()
+      ->waitForElementVisible('css', 'a.cke_button__test_media_entity_embed')
+      ->click();
+    $this->assertSession()->waitForId('drupal-modal');
+
+    // Assert autocomplete suggestions are in host entity language (fr).
+    $suggestions = $this->getAutocompleteSuggestions("Gollum n'aime que la bague");
+    $this->assertContains("Gollum n'aime que la bague", $suggestions);
+
+    // Assert autocomplete does not show suggestions for translations not
+    // matching the host entity language.
+    $suggestions = $this->getAutocompleteSuggestions('Smeagol likes cheese');
+    $this->assertNotContains('Smeagol likes cheese', $suggestions);
+
+    // Select the suggestion matching the host entity language, and proceed to
+    // the review step.
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue("Gollum n'aime que la bague (1)");
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+
+    // Assert the translated label appears, not the original.
+    $text = $form->getText();
+    $this->assertContains("Gollum n'aime que la bague", $text);
+    $this->assertNotContains('Smeagol likes cheese', $text);
+
+    // Choose to display as thumbnail with 'medium' image style.
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]')
+      ->setValue('entity_reference:media_thumbnail');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display-settings][image_style]')
+      ->setValue('medium');
+    $this->assertSession()->elementExists('css', 'button.button--primary')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Verify that the embedded entity preview in CKEditor displays the image
+    // with an `alt` attribute in the correct language (fr).
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertContains('Smeagol.jpg', $img->getAttribute('src'));
+    $this->assertEquals("Gollum n'aime que la bague alt", $img->getAttribute('alt'));
+
+    // Save the host entity, verify that it also shows up the same way on the
+    // front end, so again with an `alt` attribute in the correct language (fr).
+    // This tests the filter plugin's integration.
+    $this->getSession()->switchToIFrame();
+    $this->assertSession()->buttonExists('Save')->press();
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertContains('Smeagol.jpg', $img->getAttribute('src'));
+    $this->assertEquals("Gollum n'aime que la bague alt", $img->getAttribute('alt'));
+
+    // Verify that editing the host entity and then triggering the Entity Embed
+    // Dialog for the embedded entity again shows the embedded entity in the
+    // same language (fr).
+    $this->drupalGet('/fr/node/' . $host->id() . '/edit');
+    $this->waitForEditor();
+    $select_and_edit_embed = "var editor = CKEDITOR.instances['edit-body-0-value'];
+      var entityEmbed = editor.widgets.getByElement(editor.editable().findOne('div'));
+      entityEmbed.focus();
+      editor.execCommand('editdrupalentity');";
+    $this->getSession()->executeScript($select_and_edit_embed);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+    $text = $form->getText();
+    $this->assertContains("Gollum n'aime que la bague", $text);
+    $this->assertNotContains('Smeagol likes cheese', $text);
+
+    // Close the Entity Embed Dialog, and enter CKEditor's "source" mode.
+    $this->assertSession()->elementExists('css', '.ui-dialog-titlebar-close')->press();
+    $this->assertSession()
+      ->waitForElementVisible('css', 'a.cke_button__source')
+      ->click();
+
+    // Manually override the langcode to set it back to 'en', so that that the
+    // embed shows the original language, even though this node is translated.
+    $source = $this->assertSession()
+      ->waitForElementVisible('xpath', "//textarea[contains(@class, 'cke_source')]");
+    $value = $source->getValue();
+    $dom = Html::load($value);
+    $xpath = new \DOMXPath($dom);
+    $drupal_entity = $xpath->query('//drupal-entity')[0];
+    $drupal_entity->setAttribute("data-langcode", "en");
+    $source->setValue(Html::serialize($dom));
+
+    // Exit "source" mode.
+    $this->pressEditorButton('source');
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+
+    // Assert that the image appears with correct alt text (en).
+    $img = $this->assertSession()->waitForElementVisible('css', 'img');
+    $this->assertContains('Smeagol.jpg', $img->getAttribute('src'));
+    $this->assertEquals("Smeagol likes cheese alt", $img->getAttribute('alt'));
+
+    // Save the host entity, verify that it also shows up the same way on the
+    // front end, so again with an `alt` attribute in the correct language (en).
+    // This tests the filter plugin's integration.
+    $this->getSession()->switchToIFrame();
+    $this->assertSession()->buttonExists('Save')->press();
+
+    // Assert that the image appears with correct alt text.
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertContains('Smeagol.jpg', $img->getAttribute('src'));
+    $this->assertEquals("Smeagol likes cheese alt", $img->getAttribute('alt'));
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php
new file mode 100644
index 0000000000..8008cdc52f
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedDialogTest.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+
+/**
+ * Tests the entity_embed dialog controller and route.
+ *
+ * @group entity_embed
+ * @requires function Drupal\FunctionalJavascriptTests\WebDriverTestBase::assertSession
+ */
+class EntityEmbedDialogTest extends EntityEmbedTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['image'];
+
+  /**
+   * The test user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $webUser;
+
+  /**
+   * A test node to be used for embedding.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create a page content type.
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+
+    // Create a text format and enable the entity_embed filter.
+    $format = FilterFormat::create([
+      'format' => 'custom_format',
+      'name' => 'Custom format',
+      'filters' => [
+        'entity_embed' => [
+          'status' => 1,
+        ],
+      ],
+    ]);
+    $format->save();
+
+    $editor_group = [
+      'name' => 'Entity Embed',
+      'items' => [
+        'node',
+      ],
+    ];
+    $editor = Editor::create([
+      'format' => 'custom_format',
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [[$editor_group]],
+        ],
+      ],
+    ]);
+    $editor->save();
+
+    // Create a user with required permissions.
+    $this->webUser = $this->drupalCreateUser([
+      'access content',
+      'create page content',
+      'use text format custom_format',
+    ]);
+    $this->drupalLogin($this->webUser);
+
+    // Create a sample node to be embedded.
+    $settings = [];
+    $settings['type'] = 'page';
+    $settings['title'] = 'Embed Test Node';
+    $settings['body'] = [
+      'value' => 'This node is to be used for embedding in other nodes.',
+      'format' => 'custom_format',
+    ];
+    $this->node = $this->drupalCreateNode($settings);
+  }
+
+  /**
+   * Tests the entity embed button markup.
+   */
+  public function testEntityEmbedButtonMarkup() {
+    // Ensure that the route is accessible with a valid embed button.
+    // 'Node' embed button is provided by default by the module and hence the
+    // request must be successful.
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+
+    // Ensure form structure of the 'select' step and submit form.
+    $this->assertSession()->fieldExists('entity_id');
+
+    // Check that 'Next' is a primary button.
+    $this->assertSession()->elementExists('xpath', '//input[contains(@class, "button--primary")]');
+
+    $title = $this->node->getTitle() . ' (' . $this->node->id() . ')';
+    $this->assertSession()->fieldExists('entity_id')->setValue($title);
+    $this->assertSession()->buttonExists('Next')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $plugins = [
+      'entity_reference:entity_reference_label',
+      'entity_reference:entity_reference_entity_id',
+      'view_mode:node.full',
+      'view_mode:node.rss',
+      'view_mode:node.search_index',
+      'view_mode:node.search_result',
+      'view_mode:node.teaser',
+    ];
+    foreach ($plugins as $plugin) {
+      $this->assertSession()->optionExists('Display as', $plugin);
+    }
+
+    $this->container->get('config.factory')->getEditable('entity_embed.settings')
+      ->set('rendered_entity_mode', TRUE)->save();
+    $this->container->get('plugin.manager.entity_embed.display')->clearCachedDefinitions();
+
+    $this->drupalGet('/entity-embed/dialog/custom_format/node');
+    $title = $this->node->getTitle() . ' (' . $this->node->id() . ')';
+    $this->assertSession()->fieldExists('entity_id')->setValue($title);
+    $this->assertSession()->buttonExists('Next')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $plugins = [
+      'entity_reference:entity_reference_label',
+      'entity_reference:entity_reference_entity_id',
+      'entity_reference:entity_reference_entity_view',
+    ];
+    foreach ($plugins as $plugin) {
+      $this->assertSession()->optionExists('Display as', $plugin);
+    }
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php
new file mode 100644
index 0000000000..03283f2c75
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/EntityEmbedTestBase.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Base class for all entity_embed tests.
+ */
+abstract class EntityEmbedTestBase extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'entity_embed',
+    'entity_embed_test',
+    'node',
+    'ckeditor',
+  ];
+
+  /**
+   * Assigns a name to the CKEditor iframe, to allow use of ::switchToIFrame().
+   *
+   * @see \Behat\Mink\Session::switchToIFrame()
+   */
+  protected function assignNameToCkeditorIframe() {
+    $javascript = <<<JS
+(function(){
+  document.getElementsByClassName('cke_wysiwyg_frame')[0].id = 'ckeditor';
+})()
+JS;
+    $this->getSession()->evaluateScript($javascript);
+  }
+
+  /**
+   * Clicks a CKEditor button.
+   *
+   * @param string $name
+   *   The name of the button, such as drupalink, source, etc.
+   */
+  protected function pressEditorButton($name) {
+    $this->getSession()->switchToIFrame();
+    $this->assertSession()
+      ->waitForElementVisible('css', 'a.cke_button__' . $name)
+      ->click();
+  }
+
+  /**
+   * Waits for CKEditor to initialize.
+   *
+   * @param string $instance_id
+   *   The CKEditor instance ID.
+   * @param int $timeout
+   *   (optional) Timeout in milliseconds, defaults to 10000.
+   */
+  protected function waitForEditor($instance_id = 'edit-body-0-value', $timeout = 10000) {
+    $condition = <<<JS
+      (function() {
+        return (
+          typeof CKEDITOR !== 'undefined'
+          && typeof CKEDITOR.instances["$instance_id"] !== 'undefined'
+          && CKEDITOR.instances["$instance_id"].instanceReady
+        );
+      }());
+JS;
+
+    $this->getSession()->wait($timeout, $condition);
+  }
+
+  /**
+   * Helper function to reopen EntityEmbedDialog for first embed.
+   */
+  protected function reopenDialog() {
+    $this->getSession()->switchToIFrame();
+    $select_and_edit_embed = <<<JS
+var editor = CKEDITOR.instances['edit-body-0-value'];
+var entityEmbed = editor.widgets.getByElement(editor.editable().findOne('div'));
+entityEmbed.focus();
+editor.execCommand('editdrupalentity');
+JS;
+    $this->getSession()->executeScript($select_and_edit_embed);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+  }
+
+  /**
+   * Show visually hidden fields.
+   */
+  protected function showHiddenFields() {
+    $script = <<<JS
+      var hidden_fields = document.querySelectorAll(".visually-hidden");
+
+      [].forEach.call(hidden_fields, function(el) {
+        el.classList.remove("visually-hidden");
+      });
+JS;
+
+    $this->getSession()->executeScript($script);
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php
new file mode 100644
index 0000000000..47e95ba851
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/ImageFieldFormatterTest.php
@@ -0,0 +1,265 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+
+/**
+ * Tests ckeditor integration.
+ *
+ * @group entity_embed
+ */
+class ImageFieldFormatterTest extends WebDriverTestBase {
+
+  use SortableTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'field_ui',
+    'node',
+    'file',
+    'image',
+    'ckeditor',
+    'entity_embed',
+  ];
+
+  /**
+   * The test button.
+   *
+   * @var Drupal\embed\Entity\EmbedButton
+   */
+  protected $button;
+
+  /**
+   * The test administrative user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * Created file entity.
+   *
+   * @var \Drupal\file\FileInterface
+   */
+  protected $image;
+
+  /**
+   * File created with invalid image.
+   *
+   * @var \Drupal\file\FileInterface
+   */
+  protected $invalidImage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->button = $this->container->get('entity_type.manager')
+      ->getStorage('embed_button')
+      ->create([
+        'label' => 'Image Embed',
+        'id' => 'image_embed',
+        'type_id' => 'entity',
+        'type_settings' => [
+          'entity_type' => 'file',
+          'display_plugins' => [
+            'image:image',
+          ],
+          'entity_browser' => '',
+          'entity_browser_settings' => [
+            'display_review' => FALSE,
+          ],
+        ],
+        'icon_uuid' => NULL,
+      ]);
+
+    $this->button->save();
+
+    $format = FilterFormat::create([
+      'format' => 'embed_test',
+      'name' => 'Embed format',
+      'filters' => [],
+    ]);
+    $format->save();
+    $editor = Editor::create([
+      'format' => 'embed_test',
+      'editor' => 'ckeditor',
+      'settings' => [
+        'toolbar' => [
+          'rows' => [],
+        ],
+      ],
+    ]);
+    $editor->save();
+
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+
+    $this->adminUser = $this->drupalCreateUser([
+      'access administration pages',
+      'administer filters',
+      'administer display modes',
+      'administer embed buttons',
+      'administer site configuration',
+      'administer display modes',
+      'administer content types',
+      'administer node display',
+      'access content',
+      'create page content',
+      $format->getPermissionName(),
+    ]);
+
+    $this->drupalLogin($this->adminUser);
+
+    // Create a sample image to embed.
+    \Drupal::service('file_system')->copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://rainbow-kitten.png');
+
+    // Resize the test image so that it will be scaled down during token
+    // replacement.
+    $image1 = $this->container->get('image.factory')->get('public://rainbow-kitten.png');
+    $image1->resize(500, 500);
+    $image1->save();
+
+    $this->image = $this->container->get('entity_type.manager')
+      ->getStorage('file')
+      ->create([
+        'uri' => 'public://rainbow-kitten.png',
+        'status' => 1,
+      ]);
+    $this->image->save();
+
+    $this->invalidImage = $this->container->get('entity_type.manager')
+      ->getStorage('file')
+      ->create([
+        'uri' => 'public://nonexistentimage.jpg',
+        'filename' => 'nonexistentimage.jpg',
+        'status' => 1,
+      ]);
+    $this->invalidImage->save();
+  }
+
+  /**
+   * Test invalid image error.
+   */
+  public function testInvalidImageError() {
+    $this->drupalGet('admin/config/content/formats/manage/embed_test');
+    $this->assertSession()->buttonExists('Show group names')->press();
+    $this->assertSession()->waitForElementVisible('css', '.ckeditor-add-new-group');
+    $this->assertSession()->buttonExists('Add group')->press();
+    $this->assertSession()->waitForElementVisible('css', '[name="group-name"]')->setValue('Embeds');
+    $this->assertSession()->buttonExists('Apply')->press();
+
+    $item = 'li[data-drupal-ckeditor-button-name="' . $this->button->id() . '"]';
+    $from = "ul $item";
+    $target = 'ul.ckeditor-toolbar-group-buttons';
+
+    $this->assertSession()->waitForElementVisible('css', $target);
+    $this->sortableTo($item, $from, $target);
+
+    // Verify filter checkbox exists, then check it.
+    $page = $this->getSession()->getPage();
+    $page->checkField('filters[entity_embed][status]');
+    $page->checkField('filters[filter_html][status]');
+    $this->assertSession()->buttonExists('Show row weights')->press();
+    $page->selectFieldOption('filters[entity_embed][weight]', '0');
+    $this->assertSession()->buttonExists('Save configuration')->press();
+    $this->assertSession()->responseContains('The text format <em class="placeholder">Embed format</em> has been updated.');
+    $this->assertSession()->responseNotContains('The <em class="placeholder">Image Embed</em> button requires "alt" and "title" among the attributes of the "drupal-entity" tag within the allowed html tags.');
+
+    $filterFormat = $this->container->get('entity_type.manager')
+      ->getStorage('filter_format')
+      ->load('embed_test');
+
+    $settings = $filterFormat->filters('filter_html')->settings;
+    $allowed_html = $settings['allowed_html'];
+
+    $this->assertContains('drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title', $allowed_html);
+
+    $this->drupalGet('/node/add/page');
+    $this->assertSession()->waitForElement('css', 'a.cke_button__' . $this->button->id())->click();
+    $this->assertSession()->waitForId('drupal-modal');
+    $this->assertSession()
+      ->waitForField('entity_id')
+      ->setValue($this->invalidImage->label() . ' (' . $this->invalidImage->id() . ')');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->responseContains('The selected image "' . $this->invalidImage->label() . '" is invalid.');
+    $title = $this->image->label() . ' (' . $this->image->id() . ')';
+    $this->assertSession()->fieldExists('entity_id')->setValue($title);
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->responseNotContains('The selected image "' . $this->image->label() . '" is invalid.');
+    $this->assertSession()->responseContains('Selected entity');
+    $this->assertSession()->responseContains($this->image->label());
+    $alt_text = 'Hello world alt text';
+    $title_text = 'Hello world title text';
+    $this->assertSession()->fieldExists('attributes[alt]')->setValue($alt_text);
+    $this->assertSession()->fieldExists('attributes[title]')->setValue($title_text);
+    $this->assertSession()->elementExists('css', 'button.button--primary')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $drupal_entity = $this->assertSession()->waitForElementVisible('css', 'drupal-entity[data-embed-button="' . $this->button->id() . '"]');
+    $this->assertEquals('Hello world alt text', $drupal_entity->getAttribute('alt'));
+    $this->assertEquals('Hello world title text', $drupal_entity->getAttribute('title'));
+    $image = $drupal_entity->find('css', 'img');
+    $this->assertContains('rainbow-kitten.png', $image->getAttribute('src'));
+    $this->getSession()->switchToIFrame();
+
+    $this->assertSession()->fieldExists('title[0][value]')->setValue('Testing Page with Embed');
+    $this->assertSession()->buttonExists('Save')->press();
+
+    $wrapper = $this->assertSession()
+      ->elementExists('xpath', "//div[contains(@data-embed-button, 'image_embed')]");
+    $img = $wrapper->find('css', 'img');
+    $this->assertContains('rainbow-kitten.png', $img->getAttribute('src'));
+    $this->assertEquals('Hello world alt text', $img->getAttribute('alt'));
+    $this->assertEquals('Hello world title text', $img->getAttribute('title'));
+
+    // Test allowed_html validation.
+    $this->drupalGet('admin/config/content/formats/manage/embed_test');
+    $allowed_html_field = $this->assertSession()->fieldExists('filters[filter_html][settings][allowed_html]');
+    $base_tags = '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>';
+    $drupal_entity_no_entity_type = '<drupal-entity data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button alt title>';
+    $drupal_entity_with_alt_title = '<drupal-entity data-entity-type data-entity-uuid data-view-mode data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button alt title>';
+
+    // Verify error message when `<drupal-entity>` absent but `image_embed`
+    // button in the active toolbar.
+    $allowed_html_field->setValue($base_tags);
+    $this->assertSession()->buttonExists('Save configuration')->press();
+    $this->assertSession()->responseContains('The <em class="placeholder">Image Embed</em> button requires <code>&lt;drupal-entity&gt;</code> among the allowed HTML tags.');
+
+    // Verify error message when `<drupal-entity>` present, `alt` and `title`
+    // absent, but `image_embed` button in the active toolbar.
+    $allowed_html_field->setValue($base_tags . ' ' . $drupal_entity_no_entity_type);
+    $this->assertSession()->buttonExists('Save configuration')->press();
+    $this->assertSession()->responseContains('The <code>&lt;drupal-entity&gt;</code> tag in the allowed HTML tags is missing the following attributes: <code><em class="placeholder">data-entity-type</em></code>.');
+
+    // Verify if validation errors fixed, form is submitted successfully.
+    $allowed_html_field->setValue($base_tags . ' ' . $drupal_entity_with_alt_title);
+    $this->assertSession()->buttonExists('Save configuration')->press();
+    $this->assertSession()->responseContains('The text format <em class="placeholder">Embed format</em> has been updated.');
+  }
+
+  /**
+   * Assigns a name to the CKEditor iframe, to allow use of ::switchToIFrame().
+   *
+   * @see \Behat\Mink\Session::switchToIFrame()
+   */
+  protected function assignNameToCkeditorIframe() {
+    $javascript = <<<JS
+(function(){
+  document.getElementsByClassName('cke_wysiwyg_frame')[0].id = 'ckeditor';
+})()
+JS;
+    $this->getSession()->evaluateScript($javascript);
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php
new file mode 100644
index 0000000000..c4bb77fd58
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/MediaImageTest.php
@@ -0,0 +1,846 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+use Drupal\Component\Utility\Html;
+use Drupal\editor\Entity\Editor;
+use Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\MediaImageDecorator;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\file\Entity\File;
+use Drupal\media\Entity\Media;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\TestFileCreationTrait;
+
+/**
+ * Test Media Image specific functionality.
+ *
+ * @group entity_embed
+ */
+class MediaImageTest extends EntityEmbedTestBase {
+
+  use ContentTypeCreationTrait;
+  use TestFileCreationTrait;
+
+  /**
+   * The user to use during testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * The sample Media entity to embed.
+   *
+   * @var \Drupal\media\MediaInterface
+   */
+  protected $media;
+
+  /**
+   * A host entity with a body field to embed media in.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $host;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_embed_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Note that media_install() grants 'view media' to all users by default.
+    $this->adminUser = $this->drupalCreateUser([
+      'use text format full_html',
+      'bypass node access',
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'title' => 'Red-lipped batfish',
+    ]);
+
+    // Create a sample media entity to be embedded.
+    File::create([
+      'uri' => $this->getTestFiles('image')[0]->uri,
+    ])->save();
+    $this->media = Media::create([
+      'bundle' => 'image',
+      'name' => 'Screaming hairy armadillo',
+      'field_media_image' => [
+        [
+          'target_id' => 1,
+          'alt' => 'default alt',
+          'title' => 'default title',
+        ],
+      ],
+    ]);
+    $this->media->save();
+
+    // Create a sample host entity to embed media in.
+    $this->drupalCreateContentType(['type' => 'blog']);
+    $this->host = $this->createNode([
+      'type' => 'blog',
+      'title' => 'Animals with strange names',
+      'body' => [
+        'value' => '',
+        'format' => 'full_html',
+      ],
+    ]);
+    $this->host->save();
+
+    $this->drupalLogin($this->adminUser);
+  }
+
+  /**
+   * Tests alt and title overriding for embedded images.
+   */
+  public function testAltAndTitle() {
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+
+    $this->assignNameToCkeditorIframe();
+
+    $this->pressEditorButton('test_node');
+    $this->assertSession()->waitForId('drupal-modal');
+
+    // Test that node embed doesn't display alt and title fields.
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Red-lipped batfish (1)');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+
+    // Assert that the review step displays the selected entity with the label.
+    $text = $form->getText();
+    $this->assertContains('Red-lipped batfish', $text);
+
+    $select = $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]');
+
+    $select->setValue('view_mode:node.full');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // The view_mode:node.full display shouldn't have alt and title fields.
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][title]');
+
+    $select = $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]');
+
+    $select->setValue('view_mode:node.teaser');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // The view_mode:node.teaser display shouldn't have alt and title fields.
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][title]');
+
+    // Close the dialog.
+    $this->assertSession()->elementExists('css', '.ui-dialog-titlebar-close')->press();
+
+    // Now test with media.
+    $this->pressEditorButton('test_media_entity_embed');
+    $this->assertSession()->waitForId('drupal-modal');
+
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Screaming hairy armadillo (1)');
+    $this->assertSession()->elementExists('css', 'button.js-button-next')->click();
+    $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+
+    // Assert that the review step displays the selected entity with the label.
+    $text = $form->getText();
+    $this->assertContains('Screaming hairy armadillo', $text);
+
+    $select = $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]');
+
+    $select->setValue('entity_reference:entity_reference_entity_id');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // The entity_reference:entity_reference_entity_id display shouldn't have
+    // alt and title fields.
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][title]');
+
+    $select->setValue('entity_reference:entity_reference_label');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // The entity_reference:entity_reference_label display shouldn't have alt
+    // and title fields.
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][title]');
+
+    // Test the entity embed display that ships with core media.
+    $select->setValue('entity_reference:media_thumbnail');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]')
+      ->setValue('view_mode:media.embed');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $alt = $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertEquals($this->media->field_media_image->alt, $alt->getAttribute('placeholder'));
+    $title = $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]');
+    $this->assertEquals($this->media->field_media_image->title, $title->getAttribute('placeholder'));
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals("default alt", $img->getAttribute('alt'));
+    $this->assertEquals("default title", $img->getAttribute('title'));
+
+    $this->reopenDialog();
+
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]')
+      ->setValue('Satanic leaf-tailed gecko alt');
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]')
+      ->setValue('Satanic leaf-tailed gecko title');
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals("Satanic leaf-tailed gecko alt", $img->getAttribute('alt'));
+    $this->assertEquals("Satanic leaf-tailed gecko title", $img->getAttribute('title'));
+
+    $this->reopenDialog();
+
+    // Test a view mode that displays thumbnail field.
+    $select->setValue('view_mode:media.thumb');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]')
+      ->setValue('view_mode:media.embed');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $alt = $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertEquals('Satanic leaf-tailed gecko alt', $alt->getValue());
+    $title = $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]');
+    $this->assertEquals('Satanic leaf-tailed gecko title', $title->getValue());
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt'));
+    $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title'));
+
+    $this->reopenDialog();
+
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]')
+      ->setValue('Goblin shark alt');
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]')
+      ->setValue('Goblin shark title');
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals("Goblin shark alt", $img->getAttribute('alt'));
+    $this->assertEquals("Goblin shark title", $img->getAttribute('title'));
+
+    $this->reopenDialog();
+
+    // Test a view mode that displays the media's image field.
+    $select->setValue('view_mode:media.embed');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Test that the view_mode:media.embed display has alt and title fields,
+    // and that the default values match the values on the media's
+    // source image field.
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]')
+      ->setValue('view_mode:media.embed');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $alt = $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertEquals("Goblin shark alt", $alt->getValue());
+    $title = $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]');
+    $this->assertEquals("Goblin shark title", $title->getValue());
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals("Goblin shark alt", $img->getAttribute('alt'));
+    $this->assertEquals("Goblin shark title", $img->getAttribute('title'));
+
+    $this->reopenDialog();
+
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]')
+      ->setValue('Satanic leaf-tailed gecko alt');
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]')
+      ->setValue('Satanic leaf-tailed gecko title');
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt'));
+    $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title'));
+
+    $this->config('field.field.media.image.field_media_image')
+      ->set('settings.alt_field', FALSE)
+      ->set('settings.title_field', FALSE)
+      ->save();
+
+    drupal_flush_all_caches();
+
+    $this->reopenDialog();
+
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][title]');
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals('default alt', $img->getAttribute('alt'));
+    $this->assertEquals('default title', $img->getAttribute('title'));
+
+    $field = FieldConfig::load('media.image.field_media_image');
+    $settings = $field->getSettings();
+    $settings['alt_field'] = TRUE;
+    $field->set('settings', $settings);
+    $field->save();
+
+    drupal_flush_all_caches();
+
+    $this->reopenDialog();
+
+    // Test that when only the alt field is enabled, only alt field should
+    // display.
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][alt]')->setValue('Satanic leaf-tailed gecko alt');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][title]');
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt'));
+    $this->assertEquals('default title', $img->getAttribute('title'));
+
+    $field = FieldConfig::load('media.image.field_media_image');
+    $settings = $field->getSettings();
+    $settings['alt_field'] = FALSE;
+    $settings['title_field'] = TRUE;
+    $field->set('settings', $settings);
+    $field->save();
+
+    drupal_flush_all_caches();
+
+    $this->reopenDialog();
+
+    // With only title field enabled, only title field should display.
+    $this->assertSession()
+      ->fieldExists('attributes[data-entity-embed-display-settings][title]')->setValue('Satanic leaf-tailed gecko title');
+    $this->assertSession()
+      ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]');
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title'));
+    $this->assertEquals('default alt', $img->getAttribute('alt'));
+
+    $field = FieldConfig::load('media.image.field_media_image');
+    $settings = $field->getSettings();
+    $settings['alt_field'] = TRUE;
+    $settings['title_field'] = TRUE;
+    $field->set('settings', $settings);
+    $field->save();
+
+    drupal_flush_all_caches();
+
+    $this->reopenDialog();
+
+    // Test that setting value to double quote will allow setting the alt
+    // and title to empty.
+    $alt->setValue(MediaImageDecorator::EMPTY_STRING);
+    $title->setValue(MediaImageDecorator::EMPTY_STRING);
+
+    $this->submitDialog();
+
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEmpty($img->getAttribute('alt'));
+    $this->assertEmpty($img->getAttribute('title'));
+
+    $this->reopenDialog();
+
+    // Test that *not* filling out the fields makes it fall back to the default.
+    $alt->setValue('');
+    $title->setValue('');
+    $this->submitDialog();
+    $img = $this->assertSession()->elementExists('css', 'img');
+    $this->assertEquals('default alt', $img->getAttribute('alt'));
+    $this->assertEquals('default title', $img->getAttribute('title'));
+  }
+
+  /**
+   * Tests caption editing in the CKEditor widget.
+   */
+  public function testCkeditorWidgetHasEditableCaption() {
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+    $this->pressEditorButton('test_media_entity_embed');
+    $this->assertSession()->waitForId('drupal-modal');
+
+    // Embed media.
+    $this->assertSession()
+      ->fieldExists('entity_id')
+      ->setValue('Screaming hairy armadillo (1)');
+    $this->assertSession()
+      ->elementExists('css', 'button.js-button-next')
+      ->click();
+    $this->assertSession()
+      ->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed');
+    $this->assertSession()
+      ->selectExists('attributes[data-entity-embed-display]')
+      ->setValue('entity_reference:media_thumbnail');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()
+      ->fieldExists('attributes[data-caption]')
+      ->setValue('Is this the real life? Is this just fantasy?');
+    $this->submitDialog();
+
+    // Assert that the embedded media was upcasted to a CKEditor widget.
+    $figcaption = $this->assertSession()
+      ->elementExists('css', 'figcaption');
+    $this->assertTrue($figcaption->hasAttribute('contenteditable'));
+
+    // Type in the widget's editable for the caption.
+    $this->assertSession()->waitForElement('css', 'figcaption');
+    $this->setCaption('Caught in a <strong>landslide</strong>! No escape from <em>reality</em>!');
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->elementExists('css', 'figcaption > em');
+    $this->assertSession()->elementExists('css', 'figcaption > strong')->click();
+
+    // Select the <strong> element and unbold it.
+    $this->clickPathLinkByTitleAttribute("strong element");
+    $this->pressEditorButton('bold');
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->elementExists('css', 'figcaption > em');
+    $this->assertSession()->elementNotExists('css', 'figcaption > strong');
+
+    // Select the <em> element and unitalicize it.
+    $this->assertSession()->elementExists('css', 'figcaption > em')->click();
+    $this->clickPathLinkByTitleAttribute("em element");
+    $this->pressEditorButton('italic');
+
+    // The "source" button should reveal the HTML source in a state matching
+    // what is shown in the CKEditor widget.
+    $this->pressEditorButton('source');
+    $source = $this->assertSession()->elementExists('css', 'textarea.cke_source');
+    $value = $source->getValue();
+    $dom = Html::load($value);
+    $xpath = new \DOMXPath($dom);
+    $drupal_entity = $xpath->query('//drupal-entity')[0];
+    $this->assertEquals('Caught in a landslide! No escape from reality!', $drupal_entity->getAttribute('data-caption'));
+
+    // Change the caption by modifying the HTML source directly. When exiting
+    // "source" mode, this should be respected.
+    $poor_boy_text = "I'm just a <strong>poor boy</strong>, I need no sympathy!";
+    $drupal_entity->setAttribute("data-caption", $poor_boy_text);
+    $source->setValue(Html::serialize($dom));
+    $this->pressEditorButton('source');
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $figcaption = $this->assertSession()->waitForElement('css', 'figcaption');
+    $this->assertEquals($poor_boy_text, $figcaption->getHtml());
+
+    // Select the <strong> element that we just set in "source" mode. This
+    // proves that it was indeed rendered by the CKEditor widget.
+    $figcaption->find('css', 'strong')->click();
+    $this->pressEditorButton('bold');
+
+    // Insert a link into the caption.
+    $this->clickPathLinkByTitleAttribute("Caption element");
+    $this->pressEditorButton('drupallink');
+    $this->assertSession()->waitForId('drupal-modal');
+    $this->assertSession()
+      ->waitForElementVisible('css', '#editor-link-dialog-form')
+      ->findField('attributes[href]')
+      ->setValue('https://www.drupal.org');
+    $this->assertSession()->elementExists('css', 'button.form-submit')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Wait for the live preview in the CKEditor widget to finish loading, then
+    // edit the link; no `data-cke-saved-href` attribute should exist on it.
+    $this->getSession()->switchToIFrame('ckeditor');
+    $figcaption = $this->assertSession()->waitForElement('css', 'figcaption');
+    $figcaption->find('css', 'a')->click();
+    $this->clickPathLinkByTitleAttribute("a element");
+    $this->pressEditorButton('drupallink');
+    $this->assertSession()->waitForId('drupal-modal');
+    $this->assertSession()
+      ->waitForElementVisible('css', '#editor-link-dialog-form')
+      ->findField('attributes[href]')
+      ->setValue('https://www.drupal.org/project/drupal');
+    $this->assertSession()->elementExists('css', 'button.form-submit')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->pressEditorButton('source');
+    $source = $this->assertSession()->elementExists('css', "textarea.cke_source");
+    $value = $source->getValue();
+    $this->assertContains('https://www.drupal.org/project/drupal', $value);
+    $this->assertNotContains('data-cke-saved-href', $value);
+
+    // Save the entity.
+    $this->assertSession()->buttonExists('Save')->press();
+
+    // Verify the saved entity when viewed also contains the captioned media.
+    $link = $this->assertSession()->elementExists('css', 'figcaption > a');
+    $this->assertEquals('https://www.drupal.org/project/drupal', $link->getAttribute('href'));
+    $this->assertEquals("I'm just a poor boy, I need no sympathy!", $link->getText());
+
+    // Edit it again, type a different caption in the widget.
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForElementVisible('css', 'figcaption');
+    $this->setCaption('Scaramouch, <em>Scaramouch</em>, will you do the <strong>Fandango</strong>?');
+
+    // Verify that the element path usefully indicates the specific media type
+    // that is being embedded.
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->elementExists('xpath', '//figcaption//em')->click();
+    $this->getSession()->switchToIFrame();
+    $this->assertSession()
+      ->elementTextContains('css', '#cke_1_path', 'Embedded Media Entity Embed');
+
+    // Test that removing caption in the EntityEmbedDialog form sets the embed
+    // to be captionless.
+    $this->reopenDialog();
+    $this->assertSession()
+      ->fieldExists('attributes[data-caption]')
+      ->setValue('');
+    $this->submitDialog();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->elementExists('css', 'drupal-entity');
+    $this->assertSession()->elementNotExists('css', 'figcaption');
+
+    // Set a caption again; this time not using the CKEditor Widget, but through
+    // the dialog. We're typing HTML in the form field, but it will have to be
+    // HTML-encoded for it to actually show up properly in the CKEditor Widget.
+    $this->reopenDialog();
+    $freddys_lament = "Mama, life had just begun. But now I've gone and <strong>thrown it all away</strong>! :(";
+    $this->assertSession()
+      ->fieldExists('attributes[data-caption]')
+      ->setValue($freddys_lament);
+    $this->submitDialog();
+    $this->assertSession()->elementExists('css', 'figcaption');
+
+    // Change the caption in the dialog to contain a link.
+    $wind_markup = '<a href="http://www.drupal.org">anyway the wind blows</a>';
+    $this->reopenDialog();
+    $this->assertSession()
+      ->fieldExists('attributes[data-caption]')
+      ->setValue($wind_markup);
+    $this->submitDialog();
+
+    // Assert the caption in the CKEditor widget was updated.
+    $figcaption = $this->assertSession()
+      ->waitForElementVisible('css', 'figcaption');
+    $this->assertEquals('anyway the wind blows', $figcaption->getText());
+
+    // Change the text of the link in the caption.
+    $gallileo = '<a href="http://www.drupal.org">Gallileo, figaro, magnifico</a>';
+    $this->reopenDialog();
+    $this->assertSession()
+      ->fieldExists('attributes[data-caption]')
+      ->setValue($gallileo);
+    $this->submitDialog();
+
+    // Assert the caption in the CKEditor widget was updated.
+    $figcaption = $this->assertSession()
+      ->waitForElementVisible('css', 'figcaption');
+    $this->assertEquals('Gallileo, figaro, magnifico', $figcaption->getText());
+
+    // Erase the caption in the CKEditor Widget, verify the <figcaption> still
+    // exists and contains placeholder text, then type something else.
+    $this->setCaption('');
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->elementContains('css', 'figcaption', '');
+    $this->assertSession()->elementAttributeContains('css', 'figcaption', 'data-placeholder', 'Enter caption here');
+    $this->setCaption('Fin.');
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->elementContains('css', 'figcaption', 'Fin.');
+  }
+
+  /**
+   * Tests linkability of the CKEditor widget when `drupalimage` is disabled.
+   */
+  public function testCkeditorWidgetIsLinkableWhenDrupalImageIsAbsent() {
+    // Remove the `drupalimage` plugin's `DrupalImage` button.
+    $editor = Editor::load('full_html');
+    $settings = $editor->getSettings();
+    $rows = $settings['toolbar']['rows'];
+    foreach ($rows as $row_key => $row) {
+      foreach ($row as $group_key => $group) {
+        foreach ($group['items'] as $item_key => $item) {
+          if ($item === 'DrupalImage') {
+            unset($settings['toolbar']['rows'][$row_key][$group_key]['items'][$item_key]);
+          }
+        }
+      }
+    }
+    $editor->setSettings($settings);
+    $editor->save();
+
+    $this->testCkeditorWidgetIsLinkable();
+  }
+
+  /**
+   * Tests linkability of the CKEditor widget.
+   */
+  public function testCkeditorWidgetIsLinkable() {
+    $this->host->body->value = '<drupal-entity data-caption="baz" data-embed-button="test_media_entity_embed" data-entity-embed-display="entity_reference:media_thumbnail" data-entity-embed-display-settings="{&quot;image_style&quot;:&quot;&quot;,&quot;image_link&quot;:&quot;&quot;}" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>';
+    $this->host->save();
+
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+
+    // Select the CKEditor Widget and click the "link" button.
+    $drupal_entity = $this->assertSession()->waitForElementVisible('css', 'drupal-entity');
+    $this->assertNotEmpty($drupal_entity);
+    $drupal_entity->click();
+    $this->pressEditorButton('drupallink');
+    $this->assertSession()->waitForId('drupal-modal');
+
+    // Enter a link in the link dialog and save.
+    $this->assertSession()
+      ->waitForElementVisible('css', '#editor-link-dialog-form')
+      ->findField('attributes[href]')
+      ->setValue('https://www.drupal.org');
+    $this->assertSession()->elementExists('css', 'button.form-submit')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Save the entity.
+    $this->assertSession()->buttonExists('Save')->press();
+
+    // Verify the saved entity when viewed also contains the linked media.
+    $this->assertSession()->elementExists('css', 'figure > a[href="https://www.drupal.org"] > div[data-embed-button="test_media_entity_embed"] > img[src*="image-test.png"]');
+
+    // Test that `drupallink` also still works independently: inserting a link
+    // is possible.
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+    $this->pressEditorButton('drupallink');
+    $this->assertSession()->waitForId('drupal-modal');
+    $this->assertSession()
+      ->waitForElementVisible('css', '#editor-link-dialog-form')
+      ->findField('attributes[href]')
+      ->setValue('https://wikipedia.org');
+    $this->assertSession()->elementExists('css', 'button.form-submit')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->elementExists('css', 'body > a[href="https://wikipedia.org"]');
+    $this->assertSession()->elementExists('css', 'body > .cke_widget_drupalentity > drupal-entity > figure > a[href="https://www.drupal.org"]');
+  }
+
+  /**
+   * Tests that only <drupal-entity> tags are processed.
+   *
+   * @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTest::testOnlyDrupalEntityTagProcessed()
+   */
+  public function testOnlyDrupalEntityTagProcessed() {
+    $embed_code = '<drupal-entity data-caption="baz" data-embed-button="test_media_entity_embed" data-entity-embed-display="entity_reference:media_thumbnail" data-entity-embed-display-settings="{&quot;image_style&quot;:&quot;&quot;,&quot;image_link&quot;:&quot;&quot;}" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>';
+    $this->host->body->value = str_replace('drupal-entity', 'p', $embed_code);
+    $this->host->save();
+
+    // Assert that `<p data-* …>` is not upcast into a CKEditor Widget.
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForElementVisible('css', 'img[src*="example.jpg"]', 1000);
+    $this->assertSession()->elementNotExists('css', 'figure');
+
+    $this->host->body->value = $embed_code;
+    $this->host->save();
+
+    // Assert that `<drupal-entity data-* …>` is upcast into a CKEditor Widget.
+    $this->getSession()->reload();
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForElementVisible('css', 'img[src*="example.jpg"]');
+    $this->assertSession()->elementExists('css', 'figure');
+  }
+
+  /**
+   * The CKEditor Widget must load a preview generated using the default theme.
+   */
+  public function testPreviewUsesDefaultThemeAndIsClientCacheable() {
+    // Make the node edit form use the admin theme, like on most Drupal sites.
+    $this->config('node.settings')
+      ->set('use_admin_theme', TRUE)
+      ->save();
+    $this->container->get('router.builder')->rebuild();
+
+    // Allow the test user to view the admin theme.
+    $this->adminUser->addRole($this->drupalCreateRole(['view the administration theme']));
+    $this->adminUser->save();
+
+    // Configure a different default and admin theme, like on most Drupal sites.
+    $this->config('system.theme')
+      ->set('default', 'stable')
+      ->set('admin', 'classy')
+      ->save();
+
+    // Assert that when looking at an embedded entity in the CKEditor Widget,
+    // the preview is generated using the default theme, not the admin theme.
+    // @see entity_embed_test_entity_view_alter()
+    $this->host->body->value = '<drupal-entity data-caption="baz" data-embed-button="test_media_entity_embed" data-entity-embed-display="entity_reference:entity_reference_entity_view" data-entity-embed-display-settings="full" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>';
+    $this->host->save();
+    $this->drupalGet($this->host->toUrl('edit-form'));
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForElementVisible('css', 'img[src*="image-test.png"]');
+    $element = $this->assertSession()->elementExists('css', '[data-entity-embed-test-active-theme]');
+    $this->assertSame('stable', $element->getAttribute('data-entity-embed-test-active-theme'));
+
+    // Assert that the first preview request transferred data over the wire.
+    // Then toggle source mode on and off. This causes the CKEditor widget to be
+    // destroyed and then reconstructed. Assert that during this reconstruction,
+    // a second request is sent. This second request should have transferred 0
+    // bytes: the browser should have cached the response, thus resulting in a
+    // much better user experience.
+    $this->assertGreaterThan(0, $this->getLastPreviewRequestTransferSize());
+    $this->pressEditorButton('source');
+    $this->assertSession()->waitForElement('css', 'textarea.cke_source');
+    $this->pressEditorButton('source');
+    $this->assignNameToCkeditorIframe();
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertSession()->waitForElementVisible('css', 'img[src*="image-test.png"]');
+    $this->assertSame(0, $this->getLastPreviewRequestTransferSize());
+  }
+
+  /**
+   * Gets the transfer size of the last preview request.
+   *
+   * @return int
+   *   The transfer size in octets.
+   */
+  protected function getLastPreviewRequestTransferSize() {
+    $this->getSession()->switchToIFrame();
+    $javascript = <<<JS
+(function(){
+  return window.performance
+    .getEntries()
+    .filter(function (entry) {
+      return entry.initiatorType == 'xmlhttprequest' && entry.name.indexOf('/entity-embed/preview/') !== -1;
+    })
+    .pop()
+    .transferSize;
+})()
+JS;
+    return $this->getSession()->evaluateScript($javascript);
+  }
+
+  /**
+   * Tests even <drupal-entity> elements whose button is not present are upcast.
+   *
+   * @param string $data_embed_button_attribute
+   *   The HTML for a data-embed-button atttribute.
+   *
+   * @dataProvider providerCkeditorWidgetWorksForAllEmbeds
+   */
+  public function testCkeditorWidgetWorksForAllEmbeds($data_embed_button_attribute) {
+    $this->host->body->value = '<drupal-entity data-caption="baz" ' . $data_embed_button_attribute . ' data-entity-embed-display="entity_reference:media_thumbnail" data-entity-embed-display-settings="{&quot;image_style&quot;:&quot;&quot;,&quot;image_link&quot;:&quot;&quot;}" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-entity>';
+    $this->host->save();
+
+    $this->drupalLogin($this->adminUser);
+    $this->drupalGet('node/' . $this->host->id() . '/edit');
+    $this->waitForEditor();
+    $this->assignNameToCkeditorIframe();
+
+    $this->getSession()->switchToIFrame('ckeditor');
+    $this->assertNotNull($this->assertSession()->waitForElementVisible('css', 'figcaption'));
+  }
+
+  /**
+   * Data provider for testCkeditorWidgetWorksForAllEmbeds().
+   */
+  public function providerCkeditorWidgetWorksForAllEmbeds() {
+    return [
+      'present and active CKEditor button ID' => [
+        'data-embed-button="test_media_entity_embed"',
+      ],
+      'present and inactive CKEditor button ID' => [
+        'data-embed-button="user"',
+      ],
+      'present and nonsensical CKEditor button ID' => [
+        'data-embed-button="ceci nest pas une pipe"',
+      ],
+      'absent' => [
+        '',
+      ],
+    ];
+  }
+
+  /**
+   * Helper function to submit dialog and focus on ckeditor frame.
+   */
+  protected function submitDialog() {
+    $this->assertSession()->elementExists('css', 'button.button--primary')->press();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->getSession()->switchToIFrame('ckeditor');
+  }
+
+  /**
+   * Set the text of the editable caption to the given text.
+   *
+   * @param string $text
+   *   The text to set in the caption.
+   */
+  protected function setCaption($text) {
+    $this->getSession()->switchToIFrame();
+    $select_and_edit_caption = "var editor = CKEDITOR.instances['edit-body-0-value'];
+       var figcaption = editor.widgets.getByElement(editor.editable().findOne('figcaption'));
+       figcaption.editables.caption.setData('" . $text . "')";
+    $this->getSession()->executeScript($select_and_edit_caption);
+  }
+
+  /**
+   * Clicks a link in the editor's path links with the given title text.
+   *
+   * @param string $text
+   *   The title attribute of the link to click.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  protected function clickPathLinkByTitleAttribute($text) {
+    $this->getSession()->switchToIFrame();
+    $selector = '//span[@id="cke_1_path"]//a[@title="' . $text . '"]';
+    $this->assertSession()->elementExists('xpath', $selector)->click();
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php b/web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php
new file mode 100644
index 0000000000..3d1891a424
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/FunctionalJavascript/SortableTestTrait.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\FunctionalJavascript;
+
+/**
+ * Extends \Drupal\FunctionalJavascriptTests\SortableTestTrait.
+ *
+ * This trait is a bridge to allow dragging to work on all versions of Drupal.
+ * Drupal 8.8 and higher use Sortable (i.e., the HTML5 drag and drop API), and
+ * are therefore incompatible with Chromedriver, at least for now.
+ *
+ * @see https://www.drupal.org/project/entity_embed/issues/3108151
+ *
+ * @internal
+ *   This trait is completely and totally internal and not meant to be used in
+ *   ANY way by code that is not part of the Entity Embed module. It may be
+ *   changed in any number of ways, or even deleted outright, at any time
+ *   without warning. External code should not rely on or use this trait at all.
+ *   If you need its functionality, copy it wholesale into your own code base.
+ *
+ * @todo Remove this trait entirey when Drupal 8.8 is the minimum version of
+ *   core supported by Entity Embed.
+ */
+trait SortableTestTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function sortableUpdate($item, $from, $to = NULL) {
+    $script = <<<JS
+(function () {
+  // Set backbone model after a DOM change.
+  Drupal.ckeditor.models.Model.set('isDirty', true);
+})()
+
+JS;
+
+    $options = [
+      'script' => $script,
+      'args'   => [],
+    ];
+
+    $this->getSession()->getDriver()->getWebDriverSession()->execute($options);
+  }
+
+  /**
+   * Simulates a drag on an element from one container to another.
+   *
+   * @param string $item
+   *   The HTML selector for the element to be moved.
+   * @param string $from
+   *   The HTML selector for the previous container element.
+   * @param null|string $to
+   *   The HTML selector for the target container.
+   */
+  protected function sortableTo($item, $from, $to) {
+    // Versions of Drupal older than 8.8 allow normal Selenium-style dragging
+    // and dropping.
+    if (version_compare(\Drupal::VERSION, '8.8.0', '<')) {
+      $this->doLegacyDrag($item, $to);
+      return;
+    }
+
+    $item = addslashes($item);
+    $from = addslashes($from);
+    $to   = addslashes($to);
+
+    $script = <<<JS
+(function (src, to) {
+  var sourceElement = document.querySelector(src);
+  var toElement = document.querySelector(to);
+
+  toElement.insertBefore(sourceElement, toElement.firstChild);
+})('{$item}', '{$to}')
+
+JS;
+
+    $options = [
+      'script' => $script,
+      'args'   => [],
+    ];
+
+    $this->getSession()->getDriver()->getWebDriverSession()->execute($options);
+    $this->sortableUpdate($item, $from, $to);
+  }
+
+  /**
+   * Simulates a drag moving an element after its sibling in the same container.
+   *
+   * @param string $item
+   *   The HTML selector for the element to be moved.
+   * @param string $target
+   *   The HTML selector for the sibling element.
+   * @param string $from
+   *   The HTML selector for the element container.
+   */
+  protected function sortableAfter($item, $target, $from) {
+    $item   = addslashes($item);
+    $target = addslashes($target);
+    $from   = addslashes($from);
+
+    $script = <<<JS
+(function (src, to) {
+  var sourceElement = document.querySelector(src);
+  var toElement = document.querySelector(to);
+
+  toElement.insertAdjacentElement('afterend', sourceElement);
+})('{$item}', '{$target}')
+
+JS;
+
+    $options = [
+      'script' => $script,
+      'args'   => [],
+    ];
+
+    $this->getSession()->getDriver()->getWebDriverSession()->execute($options);
+    $this->sortableUpdate($item, $from);
+  }
+
+  protected function doLegacyDrag($item, $target) {
+    $assert_session = $this->assertSession();
+    $target = $assert_session->elementExists('css', $target);
+    $assert_session->elementExists('css', $item)->dragTo($target);
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php
new file mode 100644
index 0000000000..f47ada07a8
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterDisabledIntegrationsTest.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+/**
+ * Tests that entity embed disables certain integrations.
+ *
+ * @coversDefaultClass \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter
+ * @group entity_embed
+ */
+class EntityEmbedFilterDisabledIntegrationsTest extends EntityEmbedFilterTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'contextual',
+    'quickedit',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installConfig('system');
+    $this->container->get('current_user')
+      ->addRole($this->drupalCreateRole([
+        'access contextual links',
+        'access in-place editing',
+      ]));
+  }
+
+  /**
+   * @covers \Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\EntityReferenceFieldFormatter::disableContextualLinks
+   * @covers \Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\EntityReferenceFieldFormatter::disableQuickEdit
+   * @dataProvider providerDisabledIntegrations
+   */
+  public function testDisabledIntegrations($integration_detection_selector) {
+    $text = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-view-mode' => 'teaser',
+    ]);
+
+    $this->applyFilter($text);
+    $this->assertCount(0, $this->cssSelect($integration_detection_selector));
+  }
+
+  /**
+   * Data provider for testDisabledIntegrations().
+   */
+  public function providerDisabledIntegrations() {
+    return [
+      'contextual' => [
+        'div.embedded-entity > .contextual-region',
+      ],
+      'quickedit' => [
+        'div.embedded-entity > [data-quickedit-entity-id]',
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterLegacyTest.php
new file mode 100644
index 0000000000..f060f90b32
--- /dev/null
+++ b/web/modules/entity_embed/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/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterOverridesTest.php
new file mode 100644
index 0000000000..22a4de2ac4
--- /dev/null
+++ b/web/modules/entity_embed/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/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php
new file mode 100644
index 0000000000..09ff02db5b
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTest.php
@@ -0,0 +1,454 @@
+<?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 . '"]'));
+    $this->assertHasAttributes($this->cssSelect('div.embedded-entity')[0], $expected_attributes);
+    $this->assertSame([
+      'config:filter.format.plain_text',
+      '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' => [
+        [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'teaser',
+        ],
+        'teaser',
+        [
+          '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' => [
+        [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'full',
+        ],
+        'default',
+        [
+          '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' => [
+        [
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'default',
+        ],
+        'default',
+        [
+          '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' => [
+        [
+          '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',
+        [
+          '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' => [
+        [
+          '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',
+        [
+          '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' => [
+        [
+          'data-foo' => 'bar',
+          'foo' => 'bar',
+          'data-entity-type' => 'node',
+          'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+          'data-view-mode' => 'teaser',
+        ],
+        'teaser',
+        [
+          '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 access is respected by embedding an unpublished entity.
+   *
+   * @dataProvider providerAccessUnpublished
+   */
+  public function testAccessUnpublished($allowed_to_view_unpublished, $expected_rendered, CacheableMetadata $expected_cacheability, array $expected_attachments) {
+    // 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 > [data-entity-embed-test-view-mode="teaser"]'));
+    }
+
+    // Expected bubbleable metadata.
+    $this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags());
+    $this->assertSame($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
+    $this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge());
+    $this->assertSame($expected_attachments, $result->getAttachments());
+  }
+
+  /**
+   * 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',
+            'foo:1',
+            'node:1',
+            'node_view',
+            'user:2',
+            'user_view',
+          ])
+          ->setCacheContexts(['timezone', 'user', 'user.permissions'])
+          ->setCacheMaxAge(Cache::PERMANENT),
+        ['library' => ['entity_embed/caption']],
+      ],
+    ];
+  }
+
+  /**
+   * Tests the indicator for missing entities.
+   *
+   * @dataProvider providerMissingEntityIndicator
+   */
+  public function testMissingEntityIndicator($entity_type_id, $uuid, $expected_missing_text) {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => $entity_type_id,
+      'data-entity-uuid' => $uuid,
+      'data-view-mode' => 'default',
+    ]);
+
+    // If the UUID being used in the embed is that of the sample entity, first
+    // assert that it currently results in a functional embed, then delete it.
+    if ($uuid === static::EMBEDDED_ENTITY_UUID) {
+      $this->applyFilter($content);
+      $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="default"]'));
+      $this->embeddedEntity->delete();
+    }
+
+    $this->applyFilter($content);
+    $this->assertCount(0, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="default"]'));
+    $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' => $expected_missing_text,
+      'src' => file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')),
+      'title' => $expected_missing_text,
+    ]);
+  }
+
+  /**
+   * Data provider for testMissingEntityIndicator().
+   */
+  public function providerMissingEntityIndicator() {
+    return [
+      'node; valid UUID but for a deleted entity' => [
+        'node',
+        static::EMBEDDED_ENTITY_UUID,
+        'Missing content item.',
+      ],
+      'node; invalid UUID' => [
+        'node',
+        'invalidUUID',
+        'Missing content item.',
+      ],
+      'user; invalid UUID' => [
+        'user',
+        'invalidUUID',
+        'Missing user.',
+      ],
+    ];
+  }
+
+  /**
+   * Tests that only <drupal-entity> tags are processed.
+   *
+   * @see \Drupal\Tests\entity_embed\FunctionalJavascript\MediaImageTest::testOnlyDrupalEntityTagProcessed()
+   */
+  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());
+  }
+
+  /**
+   * Tests recursive rendering protection.
+   */
+  public function testRecursionProtection() {
+    $text = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-view-mode' => 'default',
+    ]);
+
+    // Render and verify the presence of the embedded entity 20 times.
+    for ($i = 0; $i < 20; $i++) {
+      $this->applyFilter($text);
+      $this->assertCount(1, $this->cssSelect('div.embedded-entity > [data-entity-embed-test-view-mode="default"]'));
+    }
+
+    // Render a 21st time, this is exceeding the recursion limit. The entity
+    // embed markup will be stripped.
+    $this->applyFilter($text);
+    $this->assertEmpty($this->getRawContent());
+  }
+
+  /**
+   * @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, $prefix = '', $suffix = '') {
+    $content = $this->createEmbedCode([
+      'data-entity-type' => 'node',
+      'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
+      'data-view-mode' => 'teaser',
+    ] + $additional_attributes);
+    $content = $prefix . $content . $suffix;
+
+    $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], [
+      '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',
+    ]);
+    $this->assertSame([
+      'config:filter.format.plain_text',
+      '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($expected_asset_libraries, $result->getAttachments()['library']);
+  }
+
+  /**
+   * 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'],
+      ],
+      '`<a>` + `data-caption`; `filter_caption` + `entity_embed` ⇒ caption present, link preserved' => [
+        ['filter_caption', 'entity_embed'],
+        $caption_additional_attributes,
+        'figure > a[href="https://www.drupal.org"] + figcaption',
+        TRUE,
+        ['filter/caption', 'entity_embed/caption'],
+        '<a href="https://www.drupal.org">',
+        '</a>',
+      ],
+    ];
+
+    $align_additional_attributes = ['data-align' => 'center'];
+    $align_verification_selector = 'div.embedded-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,
+        TRUE,
+        $default_asset_libraries,
+      ],
+      '`<a>` + `data-align`; `filter_align` + `entity_embed` ⇒ alignment present, link preserved' => [
+        ['filter_align', 'entity_embed'],
+        $align_additional_attributes,
+        'a[href="https://www.drupal.org"] > div.embedded-entity.align-center',
+        TRUE,
+        $default_asset_libraries,
+        '<a href="https://www.drupal.org">',
+        '</a>',
+      ],
+    ];
+
+    $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'],
+      ],
+      '`<a>` + `data-caption` + `data-align`; `filter_align` + `filter_caption` + `entity_embed` ⇒ aligned caption present, link preserved' => [
+        ['filter_align', 'filter_caption', 'entity_embed'],
+        $align_additional_attributes + $caption_additional_attributes,
+        'figure.align-center > a[href="https://www.drupal.org"] + figcaption',
+        TRUE,
+        ['filter/caption', 'entity_embed/caption'],
+        '<a href="https://www.drupal.org">',
+        '</a>',
+      ],
+    ];
+
+    return $caption_test_cases + $align_test_cases + $caption_and_align_test_cases;
+  }
+
+}
diff --git a/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php
new file mode 100644
index 0000000000..fa0bc0a6e7
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTestBase.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\Tests\entity_embed\Kernel;
+
+use Drupal\Component\Utility\Html;
+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;
+
+/**
+ * Base class for Entity Embed filter tests.
+ */
+abstract class EntityEmbedFilterTestBase extends KernelTestBase {
+
+  use NodeCreationTrait {
+    createNode as drupalCreateNode;
+  }
+  use UserCreationTrait {
+    createUser as drupalCreateUser;
+    createRole as drupalCreateRole;
+  }
+  use ContentTypeCreationTrait {
+    createContentType as drupalCreateContentType;
+  }
+
+  /**
+   * The UUID to use for the embedded entity.
+   *
+   * @var string
+   */
+  const EMBEDDED_ENTITY_UUID = 'e7a3e1fe-b69b-417e-8ee4-c80cb7640e63';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'embed',
+    'entity_embed',
+    'field',
+    'filter',
+    'node',
+    'system',
+    'text',
+    'user',
+  ];
+
+  /**
+   * The sample Node entity to embed.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $embeddedEntity;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $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',
+    ]);
+    $this->container->set('current_user', $user);
+
+    // Create a sample node to be embedded.
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
+    $this->embeddedEntity = $this->drupalCreateNode([
+      'title' => 'Embed Test Node',
+      'uuid' => static::EMBEDDED_ENTITY_UUID,
+    ]);
+  }
+
+  /**
+   * Gets an embed code with given attributes.
+   *
+   * @param array $attributes
+   *   The attributes to add.
+   *
+   * @return string
+   *   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>');
+    $xpath = new \DOMXPath($dom);
+    $drupal_entity = $xpath->query('//drupal-entity')[0];
+    foreach ($attributes as $attribute => $value) {
+      $drupal_entity->setAttribute($attribute, $value);
+    }
+    return Html::serialize($dom);
+  }
+
+  /**
+   * 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.
+   *
+   * @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 of attributes.
+   */
+  protected function assertHasAttributes(\SimpleXMLElement $element, array $attributes) {
+    foreach ($attributes as $attribute => $value) {
+      $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/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php
new file mode 100644
index 0000000000..7f907aa46c
--- /dev/null
+++ b/web/modules/entity_embed/tests/src/Kernel/EntityEmbedFilterTranslationTest.php
@@ -0,0 +1,119 @@
+<?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_type.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->assertSame(
+      $this->embeddedEntity->getTranslation($expected_title_langcode)->label(),
+      (string) $this->cssSelect('div.embedded-entity')[0]
+    );
+    // 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.
+      $match_or_fallback_langcode = in_array($text_langcode, $embedded_entity_translation_languages)
+        ? $text_langcode
+        : 'en';
+      yield "text_langcode=$text_langcode (✅) ⇒ $match_or_fallback_langcode" => [
+        $text_langcode,
+        [],
+        $match_or_fallback_langcode,
+      ];
+
+      // 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',
+      ];
+    }
+  }
+
+}
-- 
GitLab