diff --git a/composer.json b/composer.json
index 8e3a9bce102b445406a28dc8fdfa5966cf3c0516..d7ad8e5963984d10a73a4734516816f4076bfe96 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",
@@ -149,7 +149,7 @@
         "drupal/module_filter": "3.1",
         "drupal/pantheon_advanced_page_cache": "1.1",
         "drupal/paragraphs": "1.12",
-        "drupal/pathauto": "1.6",
+        "drupal/pathauto": "1.8",
         "drupal/realname": "1.0.0-rc2",
         "drupal/rebuild_cache_access": "1.7",
         "drupal/recaptcha": "2.5",
@@ -175,10 +175,10 @@
         "drupal/video_embed_field": "2.4",
         "drupal/view_unpublished": "1.0-rc1",
         "drupal/views_accordion": "1.1",
-        "drupal/views_ajax_history": "1.2",
+        "drupal/views_ajax_history": "1.5",
         "drupal/views_autocomplete_filters": "1.3",
         "drupal/views_bootstrap": "3.1",
-        "drupal/views_bulk_operations": "3.4",
+        "drupal/views_bulk_operations": "3.8",
         "drupal/views_fieldsets": "3.3",
         "drupal/views_infinite_scroll": "1.7",
         "drupal/views_slideshow": "4.4",
@@ -282,10 +282,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 b3eee3a9857336263eb2f6b499b80aedc5b5626c..b4fa092ecd8a0ed198805514a09f715f17a97ca3 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": "97a7b4312e4113ee81b8d863f237ad12",
+    "content-hash": "b9be0e811209c12590dacc555ae9d6b5",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -4195,29 +4195,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"
@@ -4226,7 +4223,7 @@
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
             "license": [
-                "GPL-2.0+"
+                "GPL-2.0-or-later"
             ],
             "authors": [
                 {
@@ -4245,17 +4242,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"
             }
         },
         {
@@ -4534,41 +4533,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",
@@ -4596,6 +4588,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"
@@ -6551,35 +6547,40 @@
         },
         {
             "name": "drupal/pathauto",
-            "version": "1.6.0",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/pathauto.git",
-                "reference": "8.x-1.6"
+                "reference": "8.x-1.8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.6.zip",
-                "reference": "8.x-1.6",
-                "shasum": "eb976ae110d73c06fafb1b657adb967dd2cb8246"
+                "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.8.zip",
+                "reference": "8.x-1.8",
+                "shasum": "ede3216abb9c4f77709338d9147334c595046329"
             },
             "require": {
-                "drupal/core": "^8.6",
+                "drupal/core": "^8.8 || ^9",
                 "drupal/ctools": "*",
                 "drupal/token": "*"
             },
+            "suggest": {
+                "drupal/redirect": "When installed Pathauto will provide a new \"Update Action\" in case your URLs change. This is the recommended update action and is considered the best practice for SEO and usability."
+            },
             "type": "drupal-module",
             "extra": {
-                "branch-alias": {
-                    "dev-1.x": "1.x-dev"
-                },
                 "drupal": {
-                    "version": "8.x-1.6",
-                    "datestamp": "1575467285",
+                    "version": "8.x-1.8",
+                    "datestamp": "1588103046",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
                     }
+                },
+                "drush": {
+                    "services": {
+                        "drush.services.yml": "^9 || ^10"
+                    }
                 }
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
@@ -6607,7 +6608,9 @@
             "description": "Provides a mechanism for modules to automatically generate aliases for the content they manage.",
             "homepage": "https://www.drupal.org/project/pathauto",
             "support": {
-                "source": "https://git.drupalcode.org/project/pathauto"
+                "source": "https://cgit.drupalcode.org/pathauto",
+                "issues": "https://www.drupal.org/project/issues/pathauto",
+                "documentation": "https://www.drupal.org/docs/8/modules/pathauto"
             }
         },
         {
@@ -8038,29 +8041,26 @@
         },
         {
             "name": "drupal/views_ajax_history",
-            "version": "1.2.0",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/views_ajax_history.git",
-                "reference": "8.x-1.2"
+                "reference": "8.x-1.5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/views_ajax_history-8.x-1.2.zip",
-                "reference": "8.x-1.2",
-                "shasum": "97c19dd21327025a58deec6200e008b3c794b022"
+                "url": "https://ftp.drupal.org/files/projects/views_ajax_history-8.x-1.5.zip",
+                "reference": "8.x-1.5",
+                "shasum": "a5c83b97c97b04454b88d34ba96800cfafb779eb"
             },
             "require": {
-                "drupal/core": "*"
+                "drupal/core": "^8.8 || ^9"
             },
             "type": "drupal-module",
             "extra": {
-                "branch-alias": {
-                    "dev-1.x": "1.x-dev"
-                },
                 "drupal": {
-                    "version": "8.x-1.2",
-                    "datestamp": "1562339886",
+                    "version": "8.x-1.5",
+                    "datestamp": "1588147485",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -8216,29 +8216,32 @@
         },
         {
             "name": "drupal/views_bulk_operations",
-            "version": "3.4.0",
+            "version": "3.8.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/views_bulk_operations.git",
-                "reference": "8.x-3.4"
+                "reference": "8.x-3.8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/views_bulk_operations-8.x-3.4.zip",
-                "reference": "8.x-3.4",
-                "shasum": "549eb149f82fbf30e975155a14cd7a0d4653dfe9"
+                "url": "https://ftp.drupal.org/files/projects/views_bulk_operations-8.x-3.8.zip",
+                "reference": "8.x-3.8",
+                "shasum": "0f8a736d14e034db42de685ebd6cc5f86583375b"
             },
             "require": {
-                "drupal/core": "~8.5"
+                "drupal/core": "^8.8 || ^9"
+            },
+            "require-dev": {
+                "drush/drush": "^10"
+            },
+            "suggest": {
+                "drush/drush": "^9 || ^10"
             },
             "type": "drupal-module",
             "extra": {
-                "branch-alias": {
-                    "dev-3.x": "3.x-dev"
-                },
                 "drupal": {
-                    "version": "8.x-3.4",
-                    "datestamp": "1580924754",
+                    "version": "8.x-3.8",
+                    "datestamp": "1591296880",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -8246,13 +8249,13 @@
                 },
                 "drush": {
                     "services": {
-                        "drush.services.yml": "^9"
+                        "drush.services.yml": "^9 || ^10"
                     }
                 }
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
             "license": [
-                "GPL-2.0+"
+                "GPL-2.0-or-later"
             ],
             "authors": [
                 {
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index d01d34d36b8815e914af6479d8c1cf02ed2b6d93..1665601de5f3e1f49199f7cde891a7e6eba01cfa 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -4320,30 +4320,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"
@@ -4353,7 +4350,7 @@
         "installation-source": "dist",
         "notification-url": "https://packages.drupal.org/8/downloads",
         "license": [
-            "GPL-2.0+"
+            "GPL-2.0-or-later"
         ],
         "authors": [
             {
@@ -4372,17 +4369,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"
         }
     },
     {
@@ -4668,42 +4667,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",
@@ -4732,6 +4724,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"
@@ -6750,36 +6746,41 @@
     },
     {
         "name": "drupal/pathauto",
-        "version": "1.6.0",
-        "version_normalized": "1.6.0.0",
+        "version": "1.8.0",
+        "version_normalized": "1.8.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/pathauto.git",
-            "reference": "8.x-1.6"
+            "reference": "8.x-1.8"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.6.zip",
-            "reference": "8.x-1.6",
-            "shasum": "eb976ae110d73c06fafb1b657adb967dd2cb8246"
+            "url": "https://ftp.drupal.org/files/projects/pathauto-8.x-1.8.zip",
+            "reference": "8.x-1.8",
+            "shasum": "ede3216abb9c4f77709338d9147334c595046329"
         },
         "require": {
-            "drupal/core": "^8.6",
+            "drupal/core": "^8.8 || ^9",
             "drupal/ctools": "*",
             "drupal/token": "*"
         },
+        "suggest": {
+            "drupal/redirect": "When installed Pathauto will provide a new \"Update Action\" in case your URLs change. This is the recommended update action and is considered the best practice for SEO and usability."
+        },
         "type": "drupal-module",
         "extra": {
-            "branch-alias": {
-                "dev-1.x": "1.x-dev"
-            },
             "drupal": {
-                "version": "8.x-1.6",
-                "datestamp": "1575467285",
+                "version": "8.x-1.8",
+                "datestamp": "1588103046",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
                 }
+            },
+            "drush": {
+                "services": {
+                    "drush.services.yml": "^9 || ^10"
+                }
             }
         },
         "installation-source": "dist",
@@ -6808,7 +6809,9 @@
         "description": "Provides a mechanism for modules to automatically generate aliases for the content they manage.",
         "homepage": "https://www.drupal.org/project/pathauto",
         "support": {
-            "source": "https://git.drupalcode.org/project/pathauto"
+            "source": "https://cgit.drupalcode.org/pathauto",
+            "issues": "https://www.drupal.org/project/issues/pathauto",
+            "documentation": "https://www.drupal.org/docs/8/modules/pathauto"
         }
     },
     {
@@ -8288,30 +8291,27 @@
     },
     {
         "name": "drupal/views_ajax_history",
-        "version": "1.2.0",
-        "version_normalized": "1.2.0.0",
+        "version": "1.5.0",
+        "version_normalized": "1.5.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/views_ajax_history.git",
-            "reference": "8.x-1.2"
+            "reference": "8.x-1.5"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/views_ajax_history-8.x-1.2.zip",
-            "reference": "8.x-1.2",
-            "shasum": "97c19dd21327025a58deec6200e008b3c794b022"
+            "url": "https://ftp.drupal.org/files/projects/views_ajax_history-8.x-1.5.zip",
+            "reference": "8.x-1.5",
+            "shasum": "a5c83b97c97b04454b88d34ba96800cfafb779eb"
         },
         "require": {
-            "drupal/core": "*"
+            "drupal/core": "^8.8 || ^9"
         },
         "type": "drupal-module",
         "extra": {
-            "branch-alias": {
-                "dev-1.x": "1.x-dev"
-            },
             "drupal": {
-                "version": "8.x-1.2",
-                "datestamp": "1562339886",
+                "version": "8.x-1.5",
+                "datestamp": "1588147485",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -8472,30 +8472,33 @@
     },
     {
         "name": "drupal/views_bulk_operations",
-        "version": "3.4.0",
-        "version_normalized": "3.4.0.0",
+        "version": "3.8.0",
+        "version_normalized": "3.8.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/views_bulk_operations.git",
-            "reference": "8.x-3.4"
+            "reference": "8.x-3.8"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/views_bulk_operations-8.x-3.4.zip",
-            "reference": "8.x-3.4",
-            "shasum": "549eb149f82fbf30e975155a14cd7a0d4653dfe9"
+            "url": "https://ftp.drupal.org/files/projects/views_bulk_operations-8.x-3.8.zip",
+            "reference": "8.x-3.8",
+            "shasum": "0f8a736d14e034db42de685ebd6cc5f86583375b"
         },
         "require": {
-            "drupal/core": "~8.5"
+            "drupal/core": "^8.8 || ^9"
+        },
+        "require-dev": {
+            "drush/drush": "^10"
+        },
+        "suggest": {
+            "drush/drush": "^9 || ^10"
         },
         "type": "drupal-module",
         "extra": {
-            "branch-alias": {
-                "dev-3.x": "3.x-dev"
-            },
             "drupal": {
-                "version": "8.x-3.4",
-                "datestamp": "1580924754",
+                "version": "8.x-3.8",
+                "datestamp": "1591296880",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -8503,14 +8506,14 @@
             },
             "drush": {
                 "services": {
-                    "drush.services.yml": "^9"
+                    "drush.services.yml": "^9 || ^10"
                 }
             }
         },
         "installation-source": "dist",
         "notification-url": "https://packages.drupal.org/8/downloads",
         "license": [
-            "GPL-2.0+"
+            "GPL-2.0-or-later"
         ],
         "authors": [
             {
diff --git a/web/modules/embed/README.md b/web/modules/embed/README.md
index e2039e148e0255803e5160a20f12f2b5a869b887..e2b97ae27bbcd3c00e5c37f350febee3d3bf7b8e 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 d0003e3988887336d540b61bf5e3b0843f88de0c..0000000000000000000000000000000000000000
--- 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 e232a20d92b01b947f38037c9bbdd6718ab74ee3..545262d9ce5172ac9e1f04508890ef057295a732 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 3e2cea64b5c37511a11d920051b191be26a5bf70..5a710757321cb0123218a69763a27ee53417b1b0 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 0000000000000000000000000000000000000000..9f5ed97bfe2378cf4f28b85aad68240db93a33bf
--- /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 77349e7f34479a2384f746ce1c718aa784f0158e..2834b39c6a0cdc779bb7ad5b6add01e08ece6c47 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 0000000000000000000000000000000000000000..42a3e3640b92bbe3c28e9c61957d3479aab50955
--- /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 543d498624a1d8eaf35a696157b44fad1a462fbf..70fc5f4aea2db300956fb6ea95304ee3382dc428 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 65df04fc9b5071fb44d6f68afc5b602578c96235..42c6009368e25a86c5e50d5ff51b7a5cc4b1161f 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 6a2d5708fec597068dbd96376465471bab083485..907c213e81ad7cf8dad18c4481d4c23798f7e407 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 5ebbe7ac4760d5b5bb4bcb76234960b01a72e85a..64867f0c6123ea4112160bd963258fef263abac5 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 b6d3adfdf89ce0db60916a1bb16aceb79c92ab10..9e33882e68abd96bf58f327efedf99faf90797d4 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 8c2a5d5443d6c13b078304f5797a09d8652cfd46..aaf4102fa0eeab1e07631c97cb0b1d0f104bdf59 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 9188b95b5aafb56b48bc05f64ceac96d7ab455aa..7a653095ffdff6d18ddaacd81ca6849de1e67eda 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 2702e22f5caa43a53604665c44d033c7f3919286..5379c90f37341ede5045f338686a960751c9579a 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 63c9c98b599e40338e67d875e3ebabac2020d51a..0000000000000000000000000000000000000000
--- 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 766748f84f65922695bc9f5f10c2eaee8e832a05..0000000000000000000000000000000000000000
--- 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 5e0f7b2b9f356f7a0a034042821d400c127b709f..a118f6b4d899defe712dde9101d6a524947457ee 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 3f95b38beed6313901a30a9af83f424f9d726894..2aeb758413f31e1a901b020e2c153fbe8d6b60ca 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 b344fa9bc00a89aad3a516ba4aa7c2b0bccd0b8f..72d2045da496b0db58a9c67a5cd8cf0b1a2e4e26 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 3221f8053a73b68246b51996bd6ff0503c2fd2d3..c319f44683ab29f626ead9e4a7621a7937c33aea 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 385bc5ff002a5cf8501b682965d76b629e14c35b..00f7bb8e2f037e8413f872b9e7ecb5c50a428668 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 719562de82858f08d8c85fe910b84a95aa0964e2..06589f95c0f0e95a2315434ae3af6ed63a2ffe64 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 c73887acfccffcb8b4a2ff8ec800d27a0138ad35..e275664e039677b9414e65e80e6c9666bc5135e3 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 fbbd8f367ef65ca5f007da1b85d24834d7433f19..bf2e581b93f28342a38949333bb1eff37bd58d21 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 0000000000000000000000000000000000000000..17f990a3c46510d4802de2332e3fc716b56a3b4e
--- /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 11613ee3e5ebb04c0206b0fad8336cdd2e7738b4..1163240327a5239e9afa37e8d1cf78df925136b6 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 0000000000000000000000000000000000000000..767cf43e318f80e431b5ad5c8ad46f898b6db52d
--- /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 d815426f8c9d9a34dc4c90e536a24b66c9dc8c2e..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..ca34d46d300c5103918a4728b3cf6a1427856072
--- /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 8c29b41ef7a4cc4a0410f431a430a3691a4dfeac..0000000000000000000000000000000000000000
--- 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 7db1ba782b2b54c380ca8d1015db4f8578001f32..0000000000000000000000000000000000000000
--- 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 15a659f19a9caaa0a51511c3d61afd5cf5ed4c9f..0000000000000000000000000000000000000000
--- 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 a13275ecf7f3d28319ed8e799f8d204c4a92ca95..a11575c2f4a8b3dfd4dc105ca8488ede5dedbac5 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 c10ecf4038ad1af156d6a270cc71ec7a0ff706d8..5fd24c40b6c167bfd93b9615a8763c88571f077c 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 5dabd951cb2774a3b42d93fb8d1c7ed7c164453a..23d7f0ec3fe6d9533a9e6bed5fa4c49f34e4477d 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 0000000000000000000000000000000000000000..f453a091505b74c42d34f208711ffb06c4807b0c
--- /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 0000000000000000000000000000000000000000..d1648f4ed953bd8c60a3334a4afb93aa9718916f
--- /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 fa762ad1253abc8a541333319a5fd777b0a043b0..ec83c6e91eb0e7e896194812e40bc6ccc50d4e1f 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 b0927604f3b0d41749971a77683a618bc4bb3a98..23026e80f1a357d7f5de83bfcf745612168e81dc 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 30a70f6641a9afe1a313f9b86a9c753aa420b8ba..6ebd6fa7fce78067cd894595096095c378a4ef15 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 5eca8e772c1cbe560b28bb901d2adb3770ebc0e0..d56978dc70481de0f55ce9d4bdf2f3b052ff8491 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 ee8fbaee840f24a6ef98274ef7f42dcd1887b8b1..a531922b782a9767235adc6771be3c09a2fd6e3c 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 0c61eed6ed38539fefdc61a44b9e9c4b29caae75..57b67103c40c81dbb774f2664f3b339b3f00a47b 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 076de85fbb3daeb95dde873eb407d22bd7f10380..9202c46bff26dbe49005eb2c8ea4e471fd01e0d5 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 b8a5090c97268de53a6bd35b5356ec6fd212fcfb..aa620e6de0a034b9ab71d5abc51198264d67c121 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 776ef82f6957ac5d4036b75374606429864f3cb0..3339088ca00a40c2548f4716f2cc1dd248770df3 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 0000000000000000000000000000000000000000..33102d9a14eae966e32979de7f017c1c55d4156b
--- /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 0000000000000000000000000000000000000000..34241cc61dd3fe3a8d64b7aa70e77633ea449372
--- /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 24e716afbbdccf568c06f1c38fa363fea1ea91e1..892ec3acb599fcbf3085a6bcac8d72dff7d31364 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 ca0a41277c9eaf5f1a77347950b2ad6b4520925a..3cb86cdbacd36ed96d88edc306bfe16c2af43bc0 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 3d97fac2cd67e2c02b32a8007326054f912e431c..f35f28b0f3bff9a6dbd5e2fef2dfd5cabdb67e47 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 f2d9d69b3ffa89586cf79979b48ecdd651b0b1ea..67531e239951ce294bcc03cb91a0dd0f32e1c85b 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 fd80bf8544c6f4a0056744cb52455b2408c83fc4..038bba243568d17ee48c22524c0ca7ff0b75c184 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 fe16bd97c8c6cb090eaaed6521762067c23814f4..cc9daaf6eb612660b78497308ab836c26c91ee10 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 2a6fa14b95db68b2fc3c5760383569e2bcd32cec..6e71eaadbac2ed6349fad69302fa949d7b9dcd33 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 22740687edce3267cb1d4568f441847d1af2a32d..d420acccf2bdfd3d6c6e0d17dc8d268343fab4bd 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 f23d70e032ef17902d88a9b8499fd5d33ffe8c1b..75fc762c3e2017a9b9b460486d9c38b751ce02be 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 967ffb3a13a81374fbdaa8766ac6fe9321b2ab55..554ff3d5027e914ae3d863333f5dcee9f5bf1234 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 b230b570c27affb7a2edf74723583cea1e9725e0..f244f89686e11a8b357840aeae5d9ae15a5d0598 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 318635a51b12e85cf6c163d5f4bf379854fda1dd..b9e3d3ddc94988fbb328faab2a4a26457dd6ac3a 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 ee29ae3d6a496b0e1f84d74d7fe40e9dafc56fca..7101f8512c65d30f0ec6fbd65a17855183c9c326 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 76b16413ec46163c78b5607d2ff2ab28b2eeab92..c175ded4780aa8bfb1f3e6b18590844b038c5c36 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 0000000000000000000000000000000000000000..7d319eb9e1b3b1743e55537094f66cd0a2918d13
--- /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 14a8a6a70f3fde9aecdea27d74e9f843eeadcf33..b69dae831fdacd606601bb9adab3e444e5213613 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 b7d84d9b8768d73c22fb4dd1446e79403ca4cf4a..0000000000000000000000000000000000000000
--- 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 594ff953bfe3513793176a4d0e05efb2a89b9dae..0000000000000000000000000000000000000000
--- 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 28ef108cb807465528cd6545c81e86d567ede024..5dac420b72d1aae1d9f6228b39a3c91a8ac230c8 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 e847f5294b92e6906bc1907d6c1fd7b2bde9dbea..860975cba9155cc18f15073ccb43c25a0648ae93 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 0000000000000000000000000000000000000000..f8a6bfe4de62c96e9be5a1f479320b599e9917fc
--- /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 0000000000000000000000000000000000000000..60ae55d6a5194c77f4ee90155be3e900c8d501ba
--- /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 0000000000000000000000000000000000000000..4c4338c0286583f45ff62cbf3beca35bcfd1af12
--- /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 0000000000000000000000000000000000000000..edf834f62d969583cb007b5092b944a7a678a05e
--- /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 0000000000000000000000000000000000000000..6a87a051b434b051a05a340e665ec453008ce934
--- /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 0000000000000000000000000000000000000000..54f487c9d09b3f2e11d4782ab3dda571fc216ebb
--- /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 0000000000000000000000000000000000000000..4468000706369615a72c30877f045cd901cfc092
--- /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 0000000000000000000000000000000000000000..7373631bd0c002b591729a12a3f4eac9e17cf9c6
--- /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 0000000000000000000000000000000000000000..db25535602a41e7eb7a436f4ad54598aa49a4c18
--- /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 0000000000000000000000000000000000000000..d625807b7d567b7261cdb6a260001701f403e970
--- /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 0000000000000000000000000000000000000000..8f3681d9627cad66e3b109499bbf966ed653d665
--- /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 0000000000000000000000000000000000000000..231200d59b33207ffdd905f156e7f275c2dac80a
--- /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 0000000000000000000000000000000000000000..9b9f864ffeb11743769c15c8a986ffeba35081b2
--- /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 0000000000000000000000000000000000000000..3a53b77365fc70d9d215f89023bf1e291564f6de
--- /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 0000000000000000000000000000000000000000..a65ff908d029b0359e91368cb455f535d9a12786
--- /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 5ffb92825f5afaa41ab77fbf5ab1edcfb1a8287b..2c0ac91d49902d1d6d6a6345303c1f9ed929abb4 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 b34a430c02e5603e9f1df12b9ad324c4447e366f..a8ecd923fbbdadf1d68fd1539a38a19cb17c21cc 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 7c60f948a468dae3d206fa16376099ba5600f022..d0eb4fe02ceb685282264a1c7983ba3f956b84c9 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 0000000000000000000000000000000000000000..653a78e661d8f8794f7679c6fb0bd3242cfd75b0
--- /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 0000000000000000000000000000000000000000..de684ef1f4423ebcd7156780c594fdce6e3ceb56
--- /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 0000000000000000000000000000000000000000..280469e13905748b13d2f28e36bd768540dfe81f
--- /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 0000000000000000000000000000000000000000..3daeda6ddd73e5177e985a807b9b6d9c17566518
--- /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 0000000000000000000000000000000000000000..67d305c0f209727fa9a08cb4343c75b7bc881ae3
--- /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 0000000000000000000000000000000000000000..dbd5067cd41164d85a415460bda37247b601edcb
--- /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 60367956e2b33bb0833897ac3d64bfd9e060f2a9..28ee225e59983a03ff1d002138154c53aa6c509d 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 98964e73780fa0b1537060ab990140b9a1daaa1a..99d30f97357dbb7551d8c23f804242d3d7920a6f 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 4833947083fb026e9976a08a6056cc9cdf48d1d0..c92bcecd6b737e7538a429117da866c51df93ff9 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 6ea1c13dc62851f979423c20431f237ce1a0f6e3..6ec214a6529462eac20d2d4e5b000e81d0435c18 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 e8572b26ac9051c3c03321181f0ddea34323385f..afa86b3e55ec0cd6a73a6e81432dddb72e252441 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 916578798a14b4eba7e029ce5a7861dd743dab79..397b2ecaba99468dd2c54872afe4eca8130c0213 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 ba566c6b68d9463093f8b553a3520d93b8206cb6..07b78da5b5e7f51560463c8b174b53a0c36c7308 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 19398c098771d920d11e44a31af9ffa97453fee7..d45d50624dae4b4ca58703446cdf241083241969 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 0000000000000000000000000000000000000000..d72290138b5fba286549d29ddf86ab1842c5863a
--- /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 7712da9fc21820cf565585f70bac32784811c8ac..cab2e3f44fd08a3e3ad873101e64adb479379db7 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 0000000000000000000000000000000000000000..4ed8503e86385058f602f3e11dffaa5cb8351f32
--- /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 0000000000000000000000000000000000000000..c4d97df4f41e9f4eb15c912ac5c4fe38d03db0b9
--- /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 0000000000000000000000000000000000000000..319863fbfeeaae964543593c1b34d11499361919
--- /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 0000000000000000000000000000000000000000..5e9749a0b9a8f9ff22690640d792d37d1758498f
--- /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 0000000000000000000000000000000000000000..8008cdc52f5a67152152ec12728ba44fff81c9d5
--- /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 0000000000000000000000000000000000000000..03283f2c7522b1d91fdb38e331ecf3c38f48d4f1
--- /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 0000000000000000000000000000000000000000..47e95ba851ac3eef6f27fd450f45ab41271295a5
--- /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 0000000000000000000000000000000000000000..c4bb77fd582d5c8229b38f8509b235a4a2efec3d
--- /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 0000000000000000000000000000000000000000..3d1891a42485d949176ce29733f7051551c68574
--- /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 0000000000000000000000000000000000000000..f47ada07a8a64e5e27da621d617b1991440dafd0
--- /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 0000000000000000000000000000000000000000..f060f90b321f332eb41f1691725e6d7ee9bee352
--- /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 0000000000000000000000000000000000000000..22a4de2ac49c8e082485c188c0fdc65bd8fed013
--- /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 0000000000000000000000000000000000000000..09ff02db5ba5031c694411e784420caadcb7d4a0
--- /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 0000000000000000000000000000000000000000..fa0bc0a6e77f499ef306de112bc57e5067527c6f
--- /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 0000000000000000000000000000000000000000..7f907aa46c2e293460c845472fe6f7e1e3918b07
--- /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',
+      ];
+    }
+  }
+
+}
diff --git a/web/modules/pathauto/README.md b/web/modules/pathauto/README.md
index f3c04bc7cde53a3ad0e119af6e2be90bbcd6077a..bdc27cf645a6e73a89200043e15de1ecf5ea1e3d 100644
--- a/web/modules/pathauto/README.md
+++ b/web/modules/pathauto/README.md
@@ -2,13 +2,12 @@ CONTENTS OF THIS FILE
 ---------------------
 
  * Introduction
- * Benefits
  * Requirements
  * Recommended Modules
  * Installation
  * Configuration
  * Notices
- * Faqs
+ * Troubleshooting
  * Maintainers
 
 
@@ -16,24 +15,24 @@ INTRODUCTION
 ------------
 
 The Pathauto module provides support functions for other modules to
-automatically generate aliases based on appropriate criteria, with a
+automatically generate aliases based on appropriate criteria and tokens, with a
 central settings path for site administrators.
 
 Implementations are provided for core entity types: content, taxonomy terms,
 and users (including blogs and forum pages).
 
-Pathauto also provides a way to delete large numbers of aliases.This feature
-is available at Administer > Configuration > Search and metadata > URL aliases > Delete aliases.
+Pathauto also provides a way to delete large numbers of aliases. This feature is
+available at Administer > Configuration > Search and metadata > URL aliases >
+Delete aliases.
 
+Pathauto is beneficial for search engine optimization (SEO) and for ease-of-use
+for visitors.
 
-BENEFITS
---------
+ * For a full description of the module, visit the project page:
+   https://www.drupal.org/project/pathauto
 
-Besides making the page address more reflective of its content than
-"node/138", it's important to know that modern search engines give
-heavy weight to search terms which appear in a page's URL. By
-automatically using keywords based directly on the page content in the URL,
-relevant search engine hits for your page can be significantly enhanced.
+ * To submit bug reports and feature suggestions, or track changes:
+   https://www.drupal.org/project/issues/pathauto
 
 
 REQUIREMENTS
@@ -56,71 +55,68 @@ RECOMMENDED MODULES
 INSTALLATION
 ------------
 
-Install the module as you would normally install a
-contributed Drupal module. Visit https://www.drupal.org/node/1897420 for
-further information. Note that there are two dependencies.
+ * Install as you would normally install a contributed Drupal module. Visit
+   https://www.drupal.org/node/1897420 for further information.
 
 
 CONFIGURATION
 -------------
 
-   1. Navigate to Administration > Extend and enable the Pathauto module.
-   2. Configure the module at admin/config/search/path/patterns - add a new
-      pattern by creating and clicking "Add Pathauto pattern".
-   3. Fill out "Path pattern" with fx [node:title], choose which content
-      types this applies to,give it a label (the name) and save it.
-   4. When you save new content from now on, it should automatically be
-      assigned an alternative URL.
+ 1. Configure the module at Administration > Configuration > Search and metadata
+    > URL aliases > Patterns (admin/config/search/path/patterns). Add a new
+    pattern by clicking "Add Pathauto pattern".
+ 2. Select the entity type for "Pattern Type", and provide an administrative
+    label.
+ 2. Fill out "Path pattern" with a token replacement pattern, such as
+    [node:title]. Use the "Browse available tokens" link to view available
+    variables to construct a URL alias pattern.
+ 3. Click "Save" to save your pattern. When you save new content from now on, it
+    will automatically be assigned the pathauto-configured URL alias.
 
 
 NOTICES
 -------
 
-Pathauto just adds URL aliases to content, users, and taxonomy terms.
-Because it's an alias, the standard Drupal URL (for example node/123 or
-taxonomy/term/1) will still function as normal.  If you have external links
-to your site pointing to standard Drupal URLs, or hardcoded links in a module,
-template, content or menu which point to standard Drupal URLs it will bypass
+Pathauto adds URL aliases to content, users, and taxonomy terms. Because the
+patterns are an alias, the standard Drupal URL (for example node/123 or
+taxonomy/term/1) will still function as normal. If you have external links to
+your site pointing to standard Drupal URLs, or hardcoded links in a module,
+template, content, or menu which point to standard Drupal URLs, it will bypass
 the alias set by Pathauto.
 
-There are reasons you might not want two URLs for the same content on your
-site. If this applies to you, please note that you will need to update any
-hard coded links in your content or blocks.
+There are reasons you might not want two URLs for the same content on your site.
+If this applies to you, you will need to update any hard coded links in your
+content or blocks.
 
-If you use the "system path" (i.e. node/10) for menu items and settings like
-that, Drupal will replace it with the url alias.
+If you use the "system path" (i.e. node/10) for menu items and settings, Drupal
+will replace it with the URL alias.
 
 
-FAQs
-----
+TROUBLESHOOTING
+---------------
 
-* URLs (not) Getting Replaced With Aliases?
-   Please bear in mind that only URLs passed through Drupal's Drupal's URL and
-   Link APIs will be replaced with their aliases during page output. If
-   a module or your template contains hardcoded links, such as
-   'href="node/$node->nid"', those won't get replaced with their corresponding
-   aliases.
+Q: Why are URLs not getting replaced with aliases?
+A: Only URLs passed through the Drupal URL and Link APIs will be replaced
+   with their aliases during page output. If a module or a template contains
+   hardcoded links (such as 'href="node/$node->nid"'), those will not get
+   replaced with their corresponding aliases.
 
-* Disabling Pathauto for a specific content type (or taxonomy)?
-   When the pattern for a content type is left blank, the default pattern will
-   be used. But if the default pattern is also blank, Pathauto will be disabled
+Q: How do you disable Pathauto for a specific content type (or taxonomy)?
+A: When the pattern for a content type is left blank, the default pattern will
+   be used. If the default pattern is also blank, Pathauto will be disabled
    for that content type.
 
 
 MAINTAINERS
 -----------
 
-The original module combined the functionality of Mike Ryan's autopath with
-Tommy Sundstrom's path_automatic.
-
-Significant enhancements were contributed by jdmquin @ www.bcdems.net.
-
-Matt England added the tracker support (tracker support has been removed in
-recent changes).
-
-Other suggestions and patches contributed by the Drupal community.
-
 Current maintainers:
 
  * Dave Reid - http://www.davereid.net
  * Sascha Grossenbacher - https://www.drupal.org/u/berdir
+
+The original Pathauto release combined the functionality of Mike Ryan's autopath
+with Tommy Sundstrom's path_automatic. Significant enhancements were contributed
+by jdmquin @ www.bcdems.net. Matt England added the (now deprecated) tracker
+support. Other suggestions and patches have been contributed by the Drupal
+community.
diff --git a/web/modules/pathauto/composer.json b/web/modules/pathauto/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..5f90dfab3adc42d86512b2e78cc0bd816f31c2b1
--- /dev/null
+++ b/web/modules/pathauto/composer.json
@@ -0,0 +1,26 @@
+{
+    "name": "drupal/pathauto",
+    "description": "Provides a mechanism for modules to automatically generate aliases for the content they manage.",
+    "type": "drupal-module",
+    "license": "GPL-2.0-or-later",
+    "homepage": "https://www.drupal.org/project/pathauto",
+    "support": {
+        "issues": "https://www.drupal.org/project/issues/pathauto",
+        "documentation": "https://www.drupal.org/docs/8/modules/pathauto",
+        "source": "https://cgit.drupalcode.org/pathauto"
+    },
+    "require": {
+        "drupal/token": "*",
+        "drupal/ctools": "*"
+    },
+    "suggest": {
+        "drupal/redirect": "When installed Pathauto will provide a new \"Update Action\" in case your URLs change. This is the recommended update action and is considered the best practice for SEO and usability."
+    },
+    "extra": {
+        "drush": {
+            "services": {
+                "drush.services.yml": "^9 || ^10"
+            }
+        }
+    }
+}
diff --git a/web/modules/pathauto/config/schema/pathauto_pattern.schema.yml b/web/modules/pathauto/config/schema/pathauto_pattern.schema.yml
index 14a1ae7b6dad0904e8609f4408df1fe0bb62776c..3142ec3cc5d868730a2871f747cc76d17f1c02d3 100644
--- a/web/modules/pathauto/config/schema/pathauto_pattern.schema.yml
+++ b/web/modules/pathauto/config/schema/pathauto_pattern.schema.yml
@@ -8,8 +8,6 @@ pathauto.pattern.*:
     label:
       type: label
       label: 'Label'
-    uuid:
-      type: string
     type:
       type: string
       label: 'Pattern type'
diff --git a/web/modules/pathauto/pathauto.info.yml b/web/modules/pathauto/pathauto.info.yml
index 313503d66249c290245b12f7efaebf45209bb421..91df82602679539fbbf049cd13336ffb136eed03 100644
--- a/web/modules/pathauto/pathauto.info.yml
+++ b/web/modules/pathauto/pathauto.info.yml
@@ -1,12 +1,11 @@
 name : 'Pathauto'
 description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.'
-core: 8.x
+core_version_requirement: ^8.8 || ^9
 type: module
 
 dependencies:
 - ctools:ctools
 - drupal:path
-- drupal:system (>=8.6)
 - token:token
 
 configure: entity.pathauto_pattern.collection
@@ -14,7 +13,7 @@ configure: entity.pathauto_pattern.collection
 recommends:
 - redirect:redirect
 
-# Information added by Drupal.org packaging script on 2019-12-04
-version: '8.x-1.6'
+# Information added by Drupal.org packaging script on 2020-04-28
+version: '8.x-1.8'
 project: 'pathauto'
-datestamp: 1575467315
+datestamp: 1588103048
diff --git a/web/modules/pathauto/pathauto.services.yml b/web/modules/pathauto/pathauto.services.yml
index ece821dfa7f164a52c7aeb969301d929693c2d25..c6ba82549839c10eef613c8d5926903daa8ae2d2 100644
--- a/web/modules/pathauto/pathauto.services.yml
+++ b/web/modules/pathauto/pathauto.services.yml
@@ -12,7 +12,7 @@ services:
       - { name: backend_overridable }
   pathauto.alias_uniquifier:
     class: Drupal\pathauto\AliasUniquifier
-    arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.route_provider', '@path.alias_manager']
+    arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.route_provider', '@path_alias.manager']
   pathauto.verbose_messenger:
     class: Drupal\pathauto\VerboseMessenger
     arguments: ['@config.factory', '@current_user', '@messenger']
diff --git a/web/modules/pathauto/src/AliasCleaner.php b/web/modules/pathauto/src/AliasCleaner.php
index f6c950826c818beaa34cb8f77c8117c4473fdb83..8cbf9ee352a514f863b980b36683eb36c1dd8709 100644
--- a/web/modules/pathauto/src/AliasCleaner.php
+++ b/web/modules/pathauto/src/AliasCleaner.php
@@ -231,16 +231,21 @@ public function cleanString($string, array $options = []) {
     $output = Html::decodeEntities($string);
     $output = PlainTextOutput::renderFromHtml($output);
 
+    // Replace or drop punctuation based on user settings.
+    $output = strtr($output, $this->cleanStringCache['punctuation']);
+    
     // Optionally transliterate.
     if ($this->cleanStringCache['transliterate']) {
       // If the reduce strings to letters and numbers is enabled, don't bother
       // replacing unknown characters with a question mark. Use an empty string
       // instead.
       $output = $this->transliteration->transliterate($output, $langcode, $this->cleanStringCache['reduce_ascii'] ? '' : '?');
+
+      // Replace or drop punctuation again as the transliteration process can
+      // convert special characters to punctuation.
+      $output = strtr($output, $this->cleanStringCache['punctuation']);
     }
 
-    // Replace or drop punctuation based on user settings.
-    $output = strtr($output, $this->cleanStringCache['punctuation']);
 
     // Reduce strings to letters and numbers.
     if ($this->cleanStringCache['reduce_ascii']) {
diff --git a/web/modules/pathauto/src/AliasStorageHelper.php b/web/modules/pathauto/src/AliasStorageHelper.php
index e654f6a4d5dc6d22929ce575f082e875b1d87572..8ed9b573c927117cef2288cec9d139b6ede19f60 100644
--- a/web/modules/pathauto/src/AliasStorageHelper.php
+++ b/web/modules/pathauto/src/AliasStorageHelper.php
@@ -7,9 +7,9 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Path\AliasRepositoryInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\path_alias\AliasRepositoryInterface;
 
 /**
  * Provides helper methods for accessing alias storage.
@@ -35,7 +35,7 @@ class AliasStorageHelper implements AliasStorageHelperInterface {
   /**
    * The alias repository.
    *
-   * @var \Drupal\Core\Path\AliasRepositoryInterface
+   * @var \Drupal\path_alias\AliasRepositoryInterface
    */
   protected $aliasRepository;
 
@@ -65,7 +65,7 @@ class AliasStorageHelper implements AliasStorageHelperInterface {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory.
-   * @param \Drupal\Core\Path\AliasRepositoryInterface $alias_repository
+   * @param \Drupal\path_alias\AliasRepositoryInterface $alias_repository
    *   The alias repository.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection.
diff --git a/web/modules/pathauto/src/AliasUniquifier.php b/web/modules/pathauto/src/AliasUniquifier.php
index 5adcb94e835d30a544db9401d642d548df68f061..dc8a9e332d44ea2096a26e7073291a650d6b0796 100644
--- a/web/modules/pathauto/src/AliasUniquifier.php
+++ b/web/modules/pathauto/src/AliasUniquifier.php
@@ -6,8 +6,8 @@
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\path_alias\AliasManagerInterface;
 
 /**
  * Provides a utility for creating a unique path alias.
@@ -45,7 +45,7 @@ class AliasUniquifier implements AliasUniquifierInterface {
   /**
    * The alias manager.
    *
-   * @var \Drupal\Core\Path\AliasManagerInterface
+   * @var \Drupal\path_alias\AliasManagerInterface
    */
   protected $aliasManager;
 
@@ -60,7 +60,7 @@ class AliasUniquifier implements AliasUniquifierInterface {
    *   The module handler.
    * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
    *   The route provider service.
-   * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
+   * @param \Drupal\path_alias\AliasManagerInterface $alias_manager
    *   The alias manager.
    */
   public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, AliasManagerInterface $alias_manager) {
diff --git a/web/modules/pathauto/src/Form/PatternEditForm.php b/web/modules/pathauto/src/Form/PatternEditForm.php
index f459f66d44a95c46ab148722adc2969a2d4c43c0..4c892d7dbbe45915b78062f3a94b51d766bfeeb9 100644
--- a/web/modules/pathauto/src/Form/PatternEditForm.php
+++ b/web/modules/pathauto/src/Form/PatternEditForm.php
@@ -72,7 +72,7 @@ public static function create(ContainerInterface $container) {
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager service.
    */
-  function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) {
+  public function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) {
     $this->manager = $manager;
     $this->entityTypeBundleInfo = $entity_type_bundle_info;
     $this->entityTypeManager = $entity_type_manager;
@@ -139,7 +139,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
         $default_bundles = [];
         $default_languages = [];
-        foreach ($this->entity->getSelectionConditions() as $condition_id => $condition) {
+        foreach ($this->entity->getSelectionConditions() as $condition) {
           if (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type->id(), 'node_type'])) {
             $default_bundles = $condition->getConfiguration()['bundles'];
           }
@@ -256,7 +256,7 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
             ]
           ]
         );
-        $entity->addRelationship($language_mapping, t('Language'));
+        $entity->addRelationship($language_mapping, $this->t('Language'));
       }
 
     }
diff --git a/web/modules/pathauto/src/LegacyAliasStorageHelper.php b/web/modules/pathauto/src/LegacyAliasStorageHelper.php
deleted file mode 100644
index 0051b77deb0d4aad6b428b8fb83a93150d7e0623..0000000000000000000000000000000000000000
--- a/web/modules/pathauto/src/LegacyAliasStorageHelper.php
+++ /dev/null
@@ -1,249 +0,0 @@
-<?php
-
-namespace Drupal\pathauto;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Database\Connection;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Path\AliasStorageInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\StringTranslation\TranslationInterface;
-
-/**
- * Provides helper methods for accessing alias storage.
- */
-class LegacyAliasStorageHelper implements AliasStorageHelperInterface {
-
-  use StringTranslationTrait;
-
-  /**
-   * Alias schema max length.
-   *
-   * @var int
-   */
-  protected $aliasSchemaMaxLength = 255;
-
-  /**
-   * Config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The alias storage.
-   *
-   * @var \Drupal\Core\Path\AliasStorageInterface
-   */
-  protected $aliasStorage;
-
-  /**
-   * The database connection.
-   *
-   * @var \Drupal\Core\Database\Connection
-   */
-  protected $database;
-
-  /**
-   * The messenger.
-   *
-   * @var \Drupal\pathauto\MessengerInterface
-   */
-  protected $messenger;
-
-  /**
-   * The config factory.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The config factory.
-   * @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
-   *   The alias storage.
-   * @param \Drupal\Core\Database\Connection $database
-   *   The database connection.
-   * @param MessengerInterface $messenger
-   *   The messenger.
-   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
-   *   The string translation service.
-   */
-  public function __construct(ConfigFactoryInterface $config_factory, AliasStorageInterface $alias_storage, Connection $database, MessengerInterface $messenger, TranslationInterface $string_translation) {
-    $this->configFactory = $config_factory;
-    $this->aliasStorage = $alias_storage;
-    $this->database = $database;
-    $this->messenger = $messenger;
-    $this->stringTranslation = $string_translation;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getAliasSchemaMaxLength() {
-    return $this->aliasSchemaMaxLength;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function save(array $path, $existing_alias = NULL, $op = NULL) {
-    $config = $this->configFactory->get('pathauto.settings');
-
-    // Alert users if they are trying to create an alias that is the same as the
-    // internal path.
-    if ($path['source'] == $path['alias']) {
-      $this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', ['%alias' => $path['alias']]));
-      return NULL;
-    }
-
-    // Skip replacing the current alias with an identical alias.
-    if (empty($existing_alias) || $existing_alias['alias'] != $path['alias']) {
-      $path += [
-        'pathauto' => TRUE,
-        'original' => $existing_alias,
-        'pid' => NULL,
-      ];
-
-      // If there is already an alias, respect some update actions.
-      if (!empty($existing_alias)) {
-        switch ($config->get('update_action')) {
-          case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
-            // Do not create the alias.
-            return NULL;
-
-          case PathautoGeneratorInterface::UPDATE_ACTION_LEAVE:
-            // Create a new alias instead of overwriting the existing by leaving
-            // $path['pid'] empty.
-            break;
-
-          case PathautoGeneratorInterface::UPDATE_ACTION_DELETE:
-            // The delete actions should overwrite the existing alias.
-            $path['pid'] = $existing_alias['pid'];
-            break;
-        }
-      }
-
-      // Save the path array.
-      $this->aliasStorage->save($path['source'], $path['alias'], $path['language'], $path['pid']);
-
-      if (!empty($existing_alias['pid'])) {
-        $this->messenger->addMessage($this->t(
-            'Created new alias %alias for %source, replacing %old_alias.',
-            [
-              '%alias' => $path['alias'],
-              '%source' => $path['source'],
-              '%old_alias' => $existing_alias['alias'],
-            ]
-          )
-        );
-      }
-      else {
-        $this->messenger->addMessage($this->t('Created new alias %alias for %source.', [
-          '%alias' => $path['alias'],
-          '%source' => $path['source'],
-        ]));
-      }
-
-      return $path;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-    $alias = $this->aliasStorage->load([
-      'source' => $source,
-      'langcode' => $language,
-    ]);
-    // If no alias was fetched and if a language was specified, fallbacks to
-    // undefined language.
-    if (!$alias && ($language !== LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
-      $alias = $this->aliasStorage->load([
-        'source' => $source,
-        'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-      ]);
-    }
-    return $alias;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function deleteBySourcePrefix($source) {
-    $pids = $this->loadBySourcePrefix($source);
-    if ($pids) {
-      $this->deleteMultiple($pids);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function deleteAll() {
-    $this->database->truncate('url_alias')->execute();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL) {
-    $this->deleteBySourcePrefix('/' . $entity->toUrl('canonical')->getInternalPath());
-    if (isset($default_uri) && $entity->toUrl('canonical')->toString() != $default_uri) {
-      $this->deleteBySourcePrefix($default_uri);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function loadBySourcePrefix($source) {
-    $select = $this->database->select('url_alias', 'u')
-      ->fields('u', ['pid']);
-
-    $or_group = $select->orConditionGroup()
-      ->condition('source', $source)
-      ->condition('source', rtrim($source, '/') . '/%', 'LIKE');
-
-    return $select
-      ->condition($or_group)
-      ->execute()
-      ->fetchCol();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function countBySourcePrefix($source) {
-    $select = $this->database->select('url_alias', 'u')
-      ->fields('u', ['pid']);
-
-    $or_group = $select->orConditionGroup()
-      ->condition('source', $source)
-      ->condition('source', rtrim($source, '/') . '/%', 'LIKE');
-
-    return $select
-      ->condition($or_group)
-      ->countQuery()
-      ->execute()
-      ->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function countAll() {
-    return $this->database->select('url_alias')
-      ->countQuery()
-      ->execute()
-      ->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function deleteMultiple($pids) {
-    foreach ($pids as $pid) {
-      $this->aliasStorage->delete(['pid' => $pid]);
-    }
-  }
-
-}
diff --git a/web/modules/pathauto/src/PathautoPatternListBuilder.php b/web/modules/pathauto/src/PathautoPatternListBuilder.php
index 10de0b655037fa8bb8b2d2a6c35499ebb9c3cda2..dd3ed4ef7d1a75bc88b0a7fd61335626fe0f1cf1 100644
--- a/web/modules/pathauto/src/PathautoPatternListBuilder.php
+++ b/web/modules/pathauto/src/PathautoPatternListBuilder.php
@@ -60,7 +60,7 @@ public function getDefaultOperations(EntityInterface $entity) {
     }
 
     $operations['duplicate'] = [
-      'title' => t('Duplicate'),
+      'title' => $this->t('Duplicate'),
       'weight' => 0,
       'url' => $this->ensureDestination($entity->toUrl('duplicate-form')),
     ];
diff --git a/web/modules/pathauto/src/PathautoServiceProvider.php b/web/modules/pathauto/src/PathautoServiceProvider.php
index f35faff261e20c15aa2f7c3a52b44333a55e72bf..ea05c45e287383b4f424c806836681042950e423 100644
--- a/web/modules/pathauto/src/PathautoServiceProvider.php
+++ b/web/modules/pathauto/src/PathautoServiceProvider.php
@@ -4,21 +4,19 @@
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ServiceProviderBase;
-use Symfony\Component\DependencyInjection\Reference;
 
 /**
- * Overrides the pathauto.alias_storage_helper service if needed.
+ * Remove the drush commands until path_alias module is enabled.
  */
 class PathautoServiceProvider extends ServiceProviderBase {
 
   /**
    * {@inheritdoc}
    */
-  public function alter(ContainerBuilder $container) {
-    if (version_compare(\Drupal::VERSION, '8.8', '<')) {
-      $definition = $container->getDefinition('pathauto.alias_storage_helper');
-      $definition->setClass(LegacyAliasStorageHelper::class);
-      $definition->setArgument(1, new Reference('path.alias_storage'));
+  public function register(ContainerBuilder $container) {
+    $definitions = array_keys($container->getDefinitions());
+    if (!in_array('path_alias.repository', $definitions)) {
+      $container->removeDefinition('pathauto.commands');
     }
   }
 
diff --git a/web/modules/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php b/web/modules/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php
index 13fb422e61dca0f476c15076bd1f900441217b26..37f751c1d8eefda7fcecad5848db17812cef307e 100644
--- a/web/modules/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php
+++ b/web/modules/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Plugin\Context\ContextDefinition;
 use Drupal\Core\Plugin\Context\EntityContextDefinition;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -86,8 +85,8 @@ public function getDerivativeDefinitions($base_plugin_definition) {
         $this->derivatives[$entity_type_id]['label'] = $entity_type->getLabel();
         $this->derivatives[$entity_type_id]['types'] = [$this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)];
         $this->derivatives[$entity_type_id]['provider'] = $entity_type->getProvider();
-        $this->derivatives[$entity_type_id]['context'] = [
-          $entity_type_id => EntityContextDefinition::fromEntityType($entity_type)->setLabel($this->t('@label being aliased', ['@label' => $entity_type->getLabel()]))
+        $this->derivatives[$entity_type_id]['context_definitions'] = [
+          $entity_type_id => new EntityContextDefinition("entity:$entity_type_id", $this->t('@label being aliased', ['@label' => $entity_type->getLabel()]))
         ];
       }
     }
diff --git a/web/modules/pathauto/src/Plugin/migrate/source/PathautoPattern.php b/web/modules/pathauto/src/Plugin/migrate/source/PathautoPattern.php
index 56d7b4fb90a54436cd65c83cca15fdeaa38fe6f2..a2a92c8691d09381063862dfac96ddb8f762a8a6 100644
--- a/web/modules/pathauto/src/Plugin/migrate/source/PathautoPattern.php
+++ b/web/modules/pathauto/src/Plugin/migrate/source/PathautoPattern.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\pathauto\Plugin\migrate\source;
 
-use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfo;
 use Drupal\Core\State\StateInterface;
 use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\Row;
@@ -21,18 +21,18 @@
 class PathautoPattern extends DrupalSqlBase {
 
   /**
-   * The entity type manager.
+   * The entity type bundle info.
    *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
    */
-  protected $entityTypeManager;
+  protected $entityTypeBundleInfo;
 
   /**
    * {@inheritdoc}
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_manager);
-    $this->entityTypeManager = $entity_type_manager;
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfo $entity_bundle_info) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
+    $this->entityTypeBundleInfo = $entity_bundle_info;
   }
 
   /**
@@ -45,8 +45,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_definition,
       $migration,
       $container->get('state'),
-      $container->get('entity.manager'),
-      $container->get('entity_type.manager')
+      $container->get('entity_type.manager'),
+      $container->get('entity_type.bundle.info')
     );
   }
 
@@ -104,7 +104,7 @@ public function prepareRow(Row $row) {
         $bundle = $matches[1];
 
         // Check that the bundle exists.
-        $bundles = $this->entityManager->getBundleInfo($entity_type);
+        $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type);
         if (!in_array($bundle, array_keys($bundles))) {
           // No matching bundle found in destination.
           return FALSE;
diff --git a/web/modules/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php b/web/modules/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php
index 81ef363b84c491d4b4f2f463d597123159211e0e..d01db94a44663deb07e1277b9321376b1dc0fca1 100644
--- a/web/modules/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php
+++ b/web/modules/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php
@@ -234,7 +234,7 @@ public function batchDelete(&$context) {
 
     PathautoState::bulkDelete($this->getEntityTypeId(), $pids_by_id);
     $context['sandbox']['count'] += count($pids_by_id);
-    $context['sandbox']['current'] = max($pids_by_id);
+    $context['sandbox']['current'] = !empty($pids_by_id) ? max($pids_by_id) : 0;
     $context['results']['deletions'][] = $this->getLabel();
 
     if ($context['sandbox']['count'] != $context['sandbox']['total']) {
diff --git a/web/modules/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php b/web/modules/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php
index ee8a341e8bca735e578803e19b0825350dcf2d4b..96f487e0192c5d33e30c19e050e6b3f5cd96c65c 100644
--- a/web/modules/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php
+++ b/web/modules/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php
@@ -19,7 +19,7 @@
  *   label = @Translation("Forum"),
  *   types = {"term"},
  *   provider = "forum",
- *   context = {
+ *   context_definitions = {
  *     "taxonomy_term" = @ContextDefinition("entity:taxonomy_term")
  *   }
  * )
diff --git a/web/modules/pathauto/tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.info.yml b/web/modules/pathauto/tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..039f05a5bff912b86c40010bec534a02280746c3
--- /dev/null
+++ b/web/modules/pathauto/tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.info.yml
@@ -0,0 +1,10 @@
+name: 'Pathauto custom punctuation testing module'
+type: module
+core_version_requirement: ^8.8 || ^9
+description: 'Add some uncommon punctuation to the replacement list.'
+package: Testing
+
+# Information added by Drupal.org packaging script on 2020-04-28
+version: '8.x-1.8'
+project: 'pathauto'
+datestamp: 1588103048
diff --git a/web/modules/pathauto/tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.module b/web/modules/pathauto/tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..ae95a13fdc79cf7c43c04323f440f24f0f6dba3f
--- /dev/null
+++ b/web/modules/pathauto/tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.module
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * Implements hook_pathauto_punctuation_chars_alter().
+ */
+function pathauto_custom_punctuation_test_pathauto_punctuation_chars_alter(array &$punctuation) {
+  $punctuation['copyright'] = ['value' => '©', 'name' => t('Copyright symbol')];
+}
diff --git a/web/modules/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml b/web/modules/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml
index 5c1a63e9240ba83fec13039587ff67de9669abfb..7e8fa2163d63d58c3702624abe570f9e092a25a7 100644
--- a/web/modules/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml
+++ b/web/modules/pathauto/tests/modules/pathauto_string_id_test/pathauto_string_id_test.info.yml
@@ -1,12 +1,12 @@
 name: 'Pathauto testing module'
 type: module
-core: '8.x'
+core_version_requirement: ^8.8 || ^9
 description: 'Pathauto for Entity with string ID.'
 package: Testing
 dependencies:
   - token:token
 
-# Information added by Drupal.org packaging script on 2019-12-04
-version: '8.x-1.6'
+# Information added by Drupal.org packaging script on 2020-04-28
+version: '8.x-1.8'
 project: 'pathauto'
-datestamp: 1575467315
+datestamp: 1588103048
diff --git a/web/modules/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml b/web/modules/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml
index a800d94375ffbe454119e86dcfde41ec724870f3..eaa18cfcc9d742c67e69e8f2707b12a8d87fffb9 100644
--- a/web/modules/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml
+++ b/web/modules/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml
@@ -2,11 +2,11 @@ name: 'Views Test Config'
 type: module
 description: 'Provides default views for tests.'
 package: Testing
-core: 8.x
+core_version_requirement: ^8.8 || ^9
 dependencies:
   - drupal:views
 
-# Information added by Drupal.org packaging script on 2019-12-04
-version: '8.x-1.6'
+# Information added by Drupal.org packaging script on 2020-04-28
+version: '8.x-1.8'
 project: 'pathauto'
-datestamp: 1575467315
+datestamp: 1588103048
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoBulkUpdateTest.php b/web/modules/pathauto/tests/src/Functional/PathautoBulkUpdateTest.php
index d9002267c4733ea577b25a122cb95650493722ba..1cad4f487d43d1dafa39cc4fd9c3a5be0aac1559 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoBulkUpdateTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoBulkUpdateTest.php
@@ -15,6 +15,11 @@ class PathautoBulkUpdateTest extends BrowserTestBase {
 
   use PathautoTestHelperTrait;
 
+ /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -46,7 +51,7 @@ class PathautoBulkUpdateTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     // Allow other modules to add additional permissions for the admin user.
@@ -65,7 +70,7 @@ function setUp() {
     $this->patterns['forum'] = $this->createPattern('forum', '/forums/[term:name]');
   }
 
-  function testBulkUpdate() {
+  public function testBulkUpdate() {
     // Create some nodes.
     $this->nodes = [];
     for ($i = 1; $i <= 5; $i++) {
@@ -93,7 +98,7 @@ function testBulkUpdate() {
     }
     $this->assertEntityAliasExists($this->adminUser);
     // This is the default "General discussion" forum.
-    $this->assertAliasExists(['source' => '/taxonomy/term/1']);
+    $this->assertAliasExists(['path' => '/taxonomy/term/1']);
 
     // Add a new node.
     $new_node = $this->drupalCreateNode(['path' => ['alias' => '', 'pathauto' => PathautoState::SKIP]]);
@@ -134,7 +139,7 @@ function testBulkUpdate() {
   /**
    * Tests alias generation for nodes that existed before installing Pathauto.
    */
-  function testBulkUpdateExistingContent() {
+  public function testBulkUpdateExistingContent() {
     // Create a node.
     $node = $this->drupalCreateNode();
 
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoEnablingEntityTypesTest.php b/web/modules/pathauto/tests/src/Functional/PathautoEnablingEntityTypesTest.php
index 2fe2d393460203969a8d7f4667656cacbb02cd17..0818049c5c6105454c7190a8701706e01ecc7076 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoEnablingEntityTypesTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoEnablingEntityTypesTest.php
@@ -16,6 +16,11 @@ class PathautoEnablingEntityTypesTest extends BrowserTestBase {
 
   use CommentTestTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -33,7 +38,7 @@ class PathautoEnablingEntityTypesTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     $this->drupalCreateContentType(['type' => 'article']);
@@ -55,7 +60,7 @@ function setUp() {
    * ability to define alias patterns for a given entity type works. Test with
    * the comment module, as it is not enabled by default.
    */
-  function testEnablingEntityTypes() {
+  public function testEnablingEntityTypes() {
     // Verify that the comment entity type is not available when trying to add
     // a new pattern, nor "broken".
     $this->drupalGet('/admin/config/search/path/patterns/add');
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoMassDeleteTest.php b/web/modules/pathauto/tests/src/Functional/PathautoMassDeleteTest.php
index 890e488fa3d0885983fecbb90d703322405af0bc..ad8b4d83997f2b0936dc13fb98bdd750585d086d 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoMassDeleteTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoMassDeleteTest.php
@@ -14,6 +14,11 @@ class PathautoMassDeleteTest extends BrowserTestBase {
 
   use PathautoTestHelperTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -52,7 +57,7 @@ class PathautoMassDeleteTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     $permissions = [
@@ -71,7 +76,7 @@ function setUp() {
   /**
    * Tests the deletion of all the aliases.
    */
-  function testDeleteAll() {
+  public function testDeleteAll() {
     /** @var \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper */
     $alias_storage_helper = \Drupal::service('pathauto.alias_storage_helper');
 
@@ -138,7 +143,7 @@ function testDeleteAll() {
   /**
    * Helper function to generate aliases.
    */
-  function generateAliases() {
+  public function generateAliases() {
     // Delete all aliases to avoid duplicated aliases. They will be recreated
     // below.
     $this->deleteAllAliases();
@@ -176,7 +181,7 @@ function generateAliases() {
       }
     }
     else {
-      foreach ($this->accounts as $id => $account) {
+      foreach ($this->accounts as $account) {
         $account->save();
       }
     }
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoNodeWebTest.php b/web/modules/pathauto/tests/src/Functional/PathautoNodeWebTest.php
index 2d5e67da06d3432da986710a87215e7da0af462f..f9a5bcafad0d080385b596d6c20c05d8d2370d7e 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoNodeWebTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoNodeWebTest.php
@@ -16,6 +16,11 @@ class PathautoNodeWebTest extends BrowserTestBase {
 
   use PathautoTestHelperTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -33,7 +38,7 @@ class PathautoNodeWebTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
@@ -56,7 +61,7 @@ function setUp() {
   /**
    * Tests editing nodes with different settings.
    */
-  function testNodeEditing() {
+  public function testNodeEditing() {
     // Ensure that the Pathauto checkbox is checked by default on the node add
     // form.
     $this->drupalGet('node/add/page');
@@ -144,7 +149,7 @@ function testNodeEditing() {
   /**
    * Test node operations.
    */
-  function testNodeOperations() {
+  public function testNodeOperations() {
     $node1 = $this->drupalCreateNode(['title' => 'node1']);
     $node2 = $this->drupalCreateNode(['title' => 'node2']);
 
@@ -196,7 +201,7 @@ public function testNodeState() {
     // Ensure that the pathauto field was saved to the database.
     \Drupal::entityTypeManager()->getStorage('node')->resetCache();
     $node = Node::load($node->id());
-    $this->assertIdentical($node->path->pathauto, PathautoState::SKIP);
+    $this->assertSame(PathautoState::SKIP, $node->path->pathauto);
 
     // Ensure that the manual path alias was saved and an automatic alias was not generated.
     $this->assertEntityAlias($node, '/test-alias');
@@ -243,7 +248,7 @@ public function testNodeState() {
     // Ensure that the pathauto field was saved to the database.
     \Drupal::entityTypeManager()->getStorage('node')->resetCache();
     $node = Node::load($node->id());
-    $this->assertIdentical($node->path->pathauto, PathautoState::CREATE);
+    $this->assertSame(PathautoState::CREATE, $node->path->pathauto);
 
     $this->assertEntityAlias($node, '/content/node-version-three');
     $this->assertNoEntityAliasExists($node, '/manually-edited-alias');
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoSettingsFormWebTest.php b/web/modules/pathauto/tests/src/Functional/PathautoSettingsFormWebTest.php
index 9d91b8837bbbdd3c3425a2e7cde8084baad998e5..bbbb3c0ccc69f09c758ec769631150944df643bb 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoSettingsFormWebTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoSettingsFormWebTest.php
@@ -14,6 +14,11 @@ class PathautoSettingsFormWebTest extends BrowserTestBase {
 
   use PathautoTestHelperTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -87,7 +92,7 @@ class PathautoSettingsFormWebTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     $this->drupalCreateContentType(['type' => 'article']);
@@ -107,7 +112,7 @@ function setUp() {
   /**
    * Test if the default values are shown correctly in the form.
    */
-  function testDefaultFormValues() {
+  public function testDefaultFormValues() {
     $this->drupalGet('/admin/config/search/path/settings');
     $this->assertNoFieldChecked('edit-verbose');
     $this->assertField('edit-separator', $this->defaultFormValues['separator']);
@@ -123,7 +128,7 @@ function testDefaultFormValues() {
   /**
    * Test the verbose option.
    */
-  function testVerboseOption() {
+  public function testVerboseOption() {
     $edit = ['verbose' => '1'];
     $this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration'));
     $this->assertText(t('The configuration options have been saved.'));
@@ -144,7 +149,7 @@ function testVerboseOption() {
   /**
    * Tests generating aliases with different settings.
    */
-  function testSettingsForm() {
+  public function testSettingsForm() {
     // Ensure the separator settings apply correctly.
     $this->checkAlias('My awesome content', '/content/my.awesome.content', ['separator' => '.']);
 
@@ -170,7 +175,7 @@ function testSettingsForm() {
   /**
    * Test the punctuation setting form items.
    */
-  function testPunctuationSettings() {
+  public function testPunctuationSettings() {
     // Test the replacement of punctuations.
     $settings = [];
     foreach ($this->defaultPunctuations as $key => $punctuation) {
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoTaxonomyWebTest.php b/web/modules/pathauto/tests/src/Functional/PathautoTaxonomyWebTest.php
index a0b5ddbcd706083ebaaab848f7492f88e46c9e71..9b643a1d169ce561171bf90f7c4e8588294f5d5f 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoTaxonomyWebTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoTaxonomyWebTest.php
@@ -13,6 +13,11 @@ class PathautoTaxonomyWebTest extends BrowserTestBase {
 
   use PathautoTestHelperTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -30,7 +35,7 @@ class PathautoTaxonomyWebTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     // Allow other modules to add additional permissions for the admin user.
@@ -49,12 +54,12 @@ function setUp() {
   /**
    * Basic functional testing of Pathauto with taxonomy terms.
    */
-  function testTermEditing() {
+  public function testTermEditing() {
     $this->drupalGet('admin/structure');
     $this->drupalGet('admin/structure/taxonomy');
 
     // Add vocabulary "tags".
-    $vocabulary = $this->addVocabulary(['name' => 'tags', 'vid' => 'tags']);
+    $this->addVocabulary(['name' => 'tags', 'vid' => 'tags']);
 
     // Create term for testing.
     $name = 'Testing: term name [';
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoTestHelperTrait.php b/web/modules/pathauto/tests/src/Functional/PathautoTestHelperTrait.php
index 95711cecd034a0ed72d5a3d582bf1d632ddb707d..0e6e9be8b0f07c23c0f44420bc8014ff6d7b9fd0 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoTestHelperTrait.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoTestHelperTrait.php
@@ -10,12 +10,15 @@
 use Drupal\taxonomy\VocabularyInterface;
 use Drupal\taxonomy\Entity\Vocabulary;
 use Drupal\taxonomy\Entity\Term;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Helper test class with some added functions for testing.
  */
 trait PathautoTestHelperTrait {
 
+  use PathAliasTestTrait;
+
   /**
    * Creates a pathauto pattern.
    *
@@ -76,17 +79,12 @@ public function assertToken($type, $object, $token, $expected) {
     $this->assertSame($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", ['@type' => $type, '@token' => $token, '@actual' => $tokens[$token], '@expected' => $expected]));
   }
 
-  public function saveAlias($source, $alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
-    \Drupal::service('path.alias_storage')->delete(['source' => $source, 'langcode' => $langcode]);
-    return \Drupal::service('path.alias_storage')->save($source, $alias, $langcode);
-  }
-
   public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) {
     // By default, use the entity language.
     if (!$langcode) {
       $langcode = $entity->language()->getId();
     }
-    return $this->saveAlias('/' . $entity->toUrl()->getInternalPath(), $alias, $langcode);
+    return $this->createPathAlias('/' . $entity->toUrl()->getInternalPath(), $alias, $langcode);
   }
 
   public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) {
@@ -98,7 +96,7 @@ public function assertEntityAlias(EntityInterface $entity, $expected_alias, $lan
   }
 
   public function assertEntityAliasExists(EntityInterface $entity) {
-    return $this->assertAliasExists(['source' => '/' . $entity->toUrl()->getInternalPath()]);
+    return $this->assertAliasExists(['path' => '/' . $entity->toUrl()->getInternalPath()]);
   }
 
   public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) {
@@ -110,7 +108,7 @@ public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) {
   }
 
   public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) {
-    $path = ['source' => '/' . $entity->toUrl()->getInternalPath()];
+    $path = ['path' => '/' . $entity->toUrl()->getInternalPath()];
     if (!empty($alias)) {
       $path['alias'] = $alias;
     }
@@ -118,29 +116,29 @@ public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL
   }
 
   public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
-    \Drupal::service('path.alias_manager')->cacheClear($source);
+    \Drupal::service('path_alias.manager')->cacheClear($source);
     $entity_type_manager = \Drupal::entityTypeManager();
     if ($entity_type_manager->hasDefinition('path_alias')) {
       $entity_type_manager->getStorage('path_alias')->resetCache();
     }
-    $this->assertEquals($expected_alias, \Drupal::service('path.alias_manager')->getAliasByPath($source, $langcode), t("Alias for %source with language '@language' is correct.",
+    $this->assertEquals($expected_alias, \Drupal::service('path_alias.manager')->getAliasByPath($source, $langcode), t("Alias for %source with language '@language' is correct.",
       ['%source' => $source, '@language' => $langcode]));
   }
 
   public function assertAliasExists($conditions) {
-    $path = \Drupal::service('path.alias_storage')->load($conditions);
-    $this->assertTrue($path, t('Alias with conditions @conditions found.', ['@conditions' => var_export($conditions, TRUE)]));
+    $path = $this->loadPathAliasByConditions($conditions);
+    $this->assertNotEmpty($path, t('Alias with conditions @conditions found.', ['@conditions' => var_export($conditions, TRUE)]));
     return $path;
   }
 
   public function assertNoAliasExists($conditions) {
-    $alias = \Drupal::service('path.alias_storage')->load($conditions);
-    $this->assertFalse($alias, t('Alias with conditions @conditions not found.', ['@conditions' => var_export($conditions, TRUE)]));
+    $alias = $this->loadPathAliasByConditions($conditions);
+    $this->assertEmpty($alias, t('Alias with conditions @conditions not found.', ['@conditions' => var_export($conditions, TRUE)]));
   }
 
   public function deleteAllAliases() {
     \Drupal::service('pathauto.alias_storage_helper')->deleteAll();
-    \Drupal::service('path.alias_manager')->cacheClear();
+    \Drupal::service('path_alias.manager')->cacheClear();
   }
 
   /**
diff --git a/web/modules/pathauto/tests/src/Functional/PathautoUserWebTest.php b/web/modules/pathauto/tests/src/Functional/PathautoUserWebTest.php
index 076fdd27bb75f6e67f5187bcc794cf29b5f6005f..8d5cfd8447eb5658f64e03edb9060a372ce75dd1 100644
--- a/web/modules/pathauto/tests/src/Functional/PathautoUserWebTest.php
+++ b/web/modules/pathauto/tests/src/Functional/PathautoUserWebTest.php
@@ -15,6 +15,11 @@ class PathautoUserWebTest extends BrowserTestBase {
   use PathautoTestHelperTrait;
 
   /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
+   /**
    * Modules to enable.
    *
    * @var array
@@ -31,7 +36,7 @@ class PathautoUserWebTest extends BrowserTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     // Allow other modules to add additional permissions for the admin user.
@@ -50,7 +55,7 @@ function setUp() {
   /**
    * Basic functional testing of Pathauto with users.
    */
-  function testUserEditing() {
+  public function testUserEditing() {
     // There should be no Pathauto checkbox on user forms.
     $this->drupalGet('user/' . $this->adminUser->id() . '/edit');
     $this->assertNoFieldById('path[0][pathauto]');
@@ -59,7 +64,7 @@ function testUserEditing() {
   /**
    * Test user operations.
    */
-  function testUserOperations() {
+  public function testUserOperations() {
     $account = $this->drupalCreateUser();
 
     // Delete all current URL aliases.
diff --git a/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoLocaleTest.php b/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoLocaleTest.php
index fedf22a11062880757b9a58ac3eccabd1a78d0af..596fc775d307a196ec5cd3e9691e4e15a0739b4a 100644
--- a/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoLocaleTest.php
+++ b/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoLocaleTest.php
@@ -18,6 +18,11 @@ class PathautoLocaleTest extends WebDriverTestBase {
 
   use PathautoTestHelperTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -39,7 +44,7 @@ protected function setUp() {
    * Test that when an English node is updated, its old English alias is
    * updated and its newer French alias is left intact.
    */
-  function testLanguageAliases() {
+  public function testLanguageAliases() {
 
     $this->createPattern('node', '/content/[node:title]');
 
@@ -55,8 +60,8 @@ function testLanguageAliases() {
       ]],
      ];
     $node = $this->drupalCreateNode($node);
-    $english_alias = \Drupal::service('path.alias_storage')->load(['alias' => '/english-node', 'langcode' => 'en']);
-    $this->assertTrue($english_alias, 'Alias created with proper language.');
+    $english_alias = $this->loadPathAliasByConditions(['alias' => '/english-node', 'langcode' => 'en']);
+    $this->assertNotEmpty($english_alias, 'Alias created with proper language.');
 
     // Also save a French alias that should not be left alone, even though
     // it is the newer alias.
@@ -64,7 +69,7 @@ function testLanguageAliases() {
 
     // Add an alias with the soon-to-be generated alias, causing the upcoming
     // alias update to generate a unique alias with the '-0' suffix.
-    $this->saveAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED);
+    $this->createPathAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED);
 
     // Update the node, triggering a change in the English alias.
     $node->path->pathauto = PathautoState::CREATE;
@@ -73,7 +78,7 @@ function testLanguageAliases() {
     // Check that the new English alias replaced the old one.
     $this->assertEntityAlias($node, '/content/english-node-0', 'en');
     $this->assertEntityAlias($node, '/french-node', 'fr');
-    $this->assertAliasExists(['pid' => $english_alias['pid'], 'alias' => '/content/english-node-0']);
+    $this->assertAliasExists(['id' => $english_alias->id(), 'alias' => '/content/english-node-0']);
 
     // Create a new node with the same title as before but without
     // specifying a language.
@@ -87,7 +92,7 @@ function testLanguageAliases() {
   /**
    * Test that patterns work on multilingual content.
    */
-  function testLanguagePatterns() {
+  public function testLanguagePatterns() {
 
     // Allow other modules to add additional permissions for the admin user.
     $permissions = [
diff --git a/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoUiTest.php b/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoUiTest.php
index 2d5c3168efc77e8cf2386df769e668ee399a88bd..ada2731f75c2b0d7db27b906850c4aa402368000 100644
--- a/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoUiTest.php
+++ b/web/modules/pathauto/tests/src/FunctionalJavascript/PathautoUiTest.php
@@ -16,6 +16,11 @@ class PathautoUiTest extends WebDriverTestBase {
 
   use PathautoTestHelperTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to enable.
    *
@@ -33,7 +38,7 @@ class PathautoUiTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  function setUp() {
+  protected function setUp() {
     parent::setUp();
 
     $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
@@ -52,7 +57,7 @@ function setUp() {
     $this->drupalLogin($this->adminUser);
   }
 
-  function testSettingsValidation() {
+  public function testSettingsValidation() {
     $this->drupalGet('/admin/config/search/path/settings');
 
     $this->assertSession()->fieldExists('max_length');
@@ -62,16 +67,16 @@ function testSettingsValidation() {
     $this->assertSession()->elementAttributeContains('css', '#edit-max-component-length', 'min', '1');
   }
 
-  function testPatternsWorkflow() {
-    $this->drupalPlaceBlock('local_tasks_block');
+  public function testPatternsWorkflow() {
+    $this->drupalPlaceBlock('local_tasks_block', ['id' => 'local-tasks-block']);
     $this->drupalPlaceBlock('local_actions_block');
     $this->drupalPlaceBlock('page_title_block');
 
     $this->drupalGet('admin/config/search/path');
-    $this->assertSession()->elementContains('css', '.block-local-tasks-block', 'Patterns');
-    $this->assertSession()->elementContains('css', '.block-local-tasks-block', 'Settings');
-    $this->assertSession()->elementContains('css', '.block-local-tasks-block', 'Bulk generate');
-    $this->assertSession()->elementContains('css', '.block-local-tasks-block', 'Delete aliases');
+    $this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Patterns');
+    $this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Settings');
+    $this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Bulk generate');
+    $this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Delete aliases');
 
     $this->drupalGet('admin/config/search/path/patterns');
     $this->clickLink('Add Pathauto pattern');
@@ -194,7 +199,7 @@ function testPatternsWorkflow() {
     $this->drupalPostForm(NULL, [], t('Delete'));
     $this->assertSession()->pageTextContains('The pathauto pattern Test has been deleted.');
 
-    $this->assertFalse(PathautoPattern::load('page_pattern'));
+    $this->assertEmpty(PathautoPattern::load('page_pattern'));
   }
 
 }
diff --git a/web/modules/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php b/web/modules/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php
index a08006550c8067eec3c98ff45c6340633c8afbe7..0bced80d8bf9391d70d341aa2f2a94a602237d05 100644
--- a/web/modules/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php
+++ b/web/modules/pathauto/tests/src/Kernel/PathautoEntityWithStringIdTest.php
@@ -36,6 +36,7 @@ class PathautoEntityWithStringIdTest extends KernelTestBase {
     'field',
     'token',
     'path',
+    'path_alias',
     'pathauto',
     'pathauto_string_id_test',
   ];
diff --git a/web/modules/pathauto/tests/src/Kernel/PathautoKernelTest.php b/web/modules/pathauto/tests/src/Kernel/PathautoKernelTest.php
index 29ca4f236627aa92767b8cf9b0794d559e5c387c..49d4182f5545d07fcba4799aeb2cbaf5933807fe 100644
--- a/web/modules/pathauto/tests/src/Kernel/PathautoKernelTest.php
+++ b/web/modules/pathauto/tests/src/Kernel/PathautoKernelTest.php
@@ -27,7 +27,7 @@ class PathautoKernelTest extends KernelTestBase {
 
   use PathautoTestHelperTrait;
 
-  public static $modules = ['system', 'field', 'text', 'user', 'node', 'path', 'pathauto', 'taxonomy', 'token', 'filter', 'ctools', 'language'];
+  public static $modules = ['system', 'field', 'text', 'user', 'node', 'path', 'path_alias', 'pathauto', 'pathauto_custom_punctuation_test', 'taxonomy', 'token', 'filter', 'ctools', 'language'];
 
   protected $currentUser;
 
@@ -44,7 +44,6 @@ class PathautoKernelTest extends KernelTestBase {
   public function setUp() {
     parent::setup();
 
-
     $this->installEntitySchema('user');
     $this->installEntitySchema('node');
     $this->installEntitySchema('taxonomy_term');
@@ -193,6 +192,11 @@ public function testCleanString() {
 
     // Test with default settings defined in pathauto.settings.yml.
     $this->installConfig(['pathauto']);
+
+    // Add a custom setting for the copyright symbol defined in
+    // pathauto_custom_punctuation_test_pathauto_punctuation_chars_alter().
+    $this->config('pathauto.settings')->set('punctuation.copyright', PathautoGeneratorInterface::PUNCTUATION_REMOVE);
+
     \Drupal::service('pathauto.generator')->resetCaches();
 
     $tests = [];
@@ -216,6 +220,9 @@ public function testCleanString() {
     // Transliteration.
     $tests['ľščťžýáíéňô'] = 'lsctzyaieno';
 
+    // Transliteration of special chars that are converted to punctuation.
+    $tests['© “Drupal”'] = 'drupal';
+
     foreach ($tests as $input => $expected) {
       $output = \Drupal::service('pathauto.alias_cleaner')->cleanString($input);
       $this->assertEquals($expected, $output, t("Drupal::service('pathauto.alias_cleaner')->cleanString('@input') expected '@expected', actual '@output'", [
@@ -251,18 +258,18 @@ public function testCleanAlias() {
    * Test pathauto_path_delete_multiple().
    */
   public function testPathDeleteMultiple() {
-    $this->saveAlias('/node/1', '/node-1-alias');
-    $this->saveAlias('/node/1/view', '/node-1-alias/view');
-    $this->saveAlias('/node/1', '/node-1-alias-en', 'en');
-    $this->saveAlias('/node/1', '/node-1-alias-fr', 'fr');
-    $this->saveAlias('/node/2', '/node-2-alias');
-    $this->saveAlias('/node/10', '/node-10-alias');
+    $this->createPathAlias('/node/1', '/node-1-alias');
+    $this->createPathAlias('/node/1/view', '/node-1-alias/view');
+    $this->createPathAlias('/node/1', '/node-1-alias-en', 'en');
+    $this->createPathAlias('/node/1', '/node-1-alias-fr', 'fr');
+    $this->createPathAlias('/node/2', '/node-2-alias');
+    $this->createPathAlias('/node/10', '/node-10-alias');
 
     \Drupal::service('pathauto.alias_storage_helper')->deleteBySourcePrefix('/node/1');
-    $this->assertNoAliasExists(['source' => "/node/1"]);
-    $this->assertNoAliasExists(['source' => "/node/1/view"]);
-    $this->assertAliasExists(['source' => "/node/2"]);
-    $this->assertAliasExists(['source' => "/node/10"]);
+    $this->assertNoAliasExists(['path' => "/node/1"]);
+    $this->assertNoAliasExists(['path' => "/node/1/view"]);
+    $this->assertAliasExists(['path' => "/node/2"]);
+    $this->assertAliasExists(['path' => "/node/10"]);
   }
 
   /**
@@ -293,7 +300,7 @@ public function testUpdateActions() {
     $node->setTitle('Third title');
     $node->save();
     $this->assertEntityAlias($node, '/content/third-title');
-    $this->assertAliasExists(['source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title']);
+    $this->assertAliasExists(['path' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title']);
 
     $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE);
     $config->save();
@@ -302,8 +309,8 @@ public function testUpdateActions() {
     $this->assertEntityAlias($node, '/content/fourth-title');
     $this->assertNoAliasExists(['alias' => '/content/third-title']);
     // The older second alias is not deleted yet.
-    $older_path = $this->assertAliasExists(['source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title']);
-    \Drupal::service('path.alias_storage')->delete($older_path);
+    $older_path = $this->assertAliasExists(['path' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title']);
+    \Drupal::service('entity_type.manager')->getStorage('path_alias')->delete([$older_path]);
 
     $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW);
     $config->save();
@@ -364,7 +371,7 @@ public function testPathTokens() {
   /**
    * Test using fields for path structures.
    */
-  function testParentChildPathTokens() {
+  public function testParentChildPathTokens() {
     // First create a field which will be used to create the path. It must
     // begin with a letter.
     $this->installEntitySchema('taxonomy_term');
@@ -377,7 +384,7 @@ function testParentChildPathTokens() {
     $field = FieldConfig::create(['field_storage' => $field_storage, 'bundle' => 'tags']);
     $field->save();
 
-    $display = entity_get_display('taxonomy_term', 'tags', 'default');
+    $display = \Drupal::service('entity_display.repository')->getViewDisplay('taxonomy_term', 'tags');
     $display->setComponent($fieldname, ['type' => 'string']);
     $display->save();
 
@@ -404,7 +411,7 @@ function testParentChildPathTokens() {
    */
   public function testTaxonomyPattern() {
     // Create a vocabulary and test that it's pattern variable works.
-    $vocab = $this->addVocabulary(['vid' => 'name']);
+    $this->addVocabulary(['vid' => 'name']);
     $this->createPattern('taxonomy_term', 'base');
     $pattern = $this->createPattern('taxonomy_term', 'bundle', -1);
     $this->addBundleCondition($pattern, 'taxonomy_term', 'name');
@@ -412,7 +419,7 @@ public function testTaxonomyPattern() {
     $this->assertEntityPattern('taxonomy_term', 'name', Language::LANGCODE_NOT_SPECIFIED, 'bundle');
   }
 
-  function testNoExistingPathAliases() {
+  public function testNoExistingPathAliases() {
     $this->config('pathauto.settings')
       ->set('punctuation.period', PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING)
       ->save();
@@ -445,7 +452,7 @@ function testNoExistingPathAliases() {
   /**
    * Test programmatic entity creation for aliases.
    */
-  function testProgrammaticEntityCreation() {
+  public function testProgrammaticEntityCreation() {
     $node = $this->drupalCreateNode(['title' => 'Test node', 'path' => ['pathauto' => TRUE]]);
     $this->assertEntityAlias($node, '/content/test-node');
 
@@ -475,7 +482,7 @@ function testProgrammaticEntityCreation() {
   /**
    * Tests word safe alias truncating.
    */
-  function testPathAliasUniquifyWordsafe() {
+  public function testPathAliasUniquifyWordsafe() {
     $this->config('pathauto.settings')
       ->set('max_length', 26)
       ->save();
@@ -493,7 +500,7 @@ function testPathAliasUniquifyWordsafe() {
   /**
    * Test if aliases are (not) generated with enabled/disabled patterns.
    */
-  function testPatternStatus() {
+  public function testPatternStatus() {
     // Create a node to get an alias for.
     $title = 'Pattern enabled';
     $alias = '/content/pattern-enabled';
diff --git a/web/modules/pathauto/tests/src/Kernel/PathautoTokenTest.php b/web/modules/pathauto/tests/src/Kernel/PathautoTokenTest.php
index 93a5456155631c5a4dfc62e9f0f0f51b74137bbc..95b564a3cfd7119cd0380135019dd16e36d2d58d 100644
--- a/web/modules/pathauto/tests/src/Kernel/PathautoTokenTest.php
+++ b/web/modules/pathauto/tests/src/Kernel/PathautoTokenTest.php
@@ -17,7 +17,7 @@ class PathautoTokenTest extends KernelTestBase {
    *
    * @var array
    */
-  public static $modules = ['system', 'token', 'pathauto'];
+  public static $modules = ['system', 'token', 'path_alias', 'pathauto'];
 
   public function testPathautoTokens() {
 
@@ -97,7 +97,7 @@ public function assertTokens($type, array $data, array $tokens, array $options =
         $this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", ['@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected]));
       }
       else {
-        $this->assertIdentical($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", ['@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected]));
+        $this->assertSame($expected, $replacements[$token], t("Token value for @token was '@actual', expected value '@expected'.", ['@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected]));
       }
     }
 
diff --git a/web/modules/views_ajax_history/config/schema/views_ajax_history.schema.yml b/web/modules/views_ajax_history/config/schema/views_ajax_history.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..978c9b4f14eaae877aed3be74f799ffa8fb86284
--- /dev/null
+++ b/web/modules/views_ajax_history/config/schema/views_ajax_history.schema.yml
@@ -0,0 +1,6 @@
+views.display_extender.ajax_history:
+  type: views_display_extender
+  mapping:
+    enable_history:
+      type: boolean
+      label: 'Enable history'
diff --git a/web/modules/views_ajax_history/js/views_ajax_history.js b/web/modules/views_ajax_history/js/views_ajax_history.js
index a5058acde628f9a4e18190076a991d36c30e5165..d9e4e22b875c8f7885709cfd51cf02598704d347 100644
--- a/web/modules/views_ajax_history/js/views_ajax_history.js
+++ b/web/modules/views_ajax_history/js/views_ajax_history.js
@@ -295,7 +295,8 @@
 
     if (data.view_name && options.type !== 'GET') {
       // Override the URL to not contain any fields that were submitted.
-      options.url = drupalSettings.views.ajax_path + '?' + Drupal.ajax.WRAPPER_FORMAT + '=drupal_ajax';
+      var delimiter = drupalSettings.views.ajax_path.indexOf('?') === -1 ? '?' : '&';
+      options.url = drupalSettings.views.ajax_path + delimiter + Drupal.ajax.WRAPPER_FORMAT + '=drupal_ajax';
     }
     // Call the original Drupal method with the right context.
     beforeSend.apply(this, arguments);
diff --git a/web/modules/views_ajax_history/src/Plugin/views/display_extender/AjaxHistory.php b/web/modules/views_ajax_history/src/Plugin/views/display_extender/AjaxHistory.php
new file mode 100644
index 0000000000000000000000000000000000000000..62002a9f42b635ceffba18520fbd2ae06dcec32f
--- /dev/null
+++ b/web/modules/views_ajax_history/src/Plugin/views/display_extender/AjaxHistory.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\views_ajax_history\Plugin\views\display_extender;
+
+use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Ajax history display extender plugin.
+ *
+ * @ingroup views_display_extender_plugins
+ *
+ * @ViewsDisplayExtender(
+ *   id = "ajax_history",
+ *   title = @Translation("AJAX history"),
+ *   help = @Translation("Enable the AJAX history feature for the current view."),
+ *   no_ui = FALSE,
+ * )
+ */
+class AjaxHistory extends DisplayExtenderPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    if ($form_state->get('section') == 'use_ajax') {
+      // Add opportunity to enable view ajax history handler for this view.
+      $form['enable_history'] = [
+        '#title' => $this->t('AJAX history'),
+        '#type' => 'checkbox',
+        '#description' => $this->t('Enable Views AJAX history.'),
+        '#default_value' => isset($this->options['enable_history']) ? $this->options['enable_history'] : 0,
+        '#states' => [
+          'visible' => [
+            ':input[name="use_ajax"]' => ['checked' => TRUE],
+          ],
+        ],
+      ];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
+    if ($form_state->hasValue('use_ajax') && $form_state->getValue('use_ajax') != TRUE) {
+      // Prevent use ajax history when ajax for view are disabled.
+      $form_state->setValue('enable_history', FALSE);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitOptionsForm(&$form, FormStateInterface $form_state) {
+    if ($form_state->get('section') == 'use_ajax') {
+      $this->options['enable_history'] = $form_state->getValue('enable_history');
+    }
+  }
+
+}
diff --git a/web/modules/views_ajax_history/views_ajax_history.info.yml b/web/modules/views_ajax_history/views_ajax_history.info.yml
index 5595d6d6bbd67d61c9a60aa0cbcff1dfb11e859e..05af105db314638f69ee87f0cffeda803efbeb44 100644
--- a/web/modules/views_ajax_history/views_ajax_history.info.yml
+++ b/web/modules/views_ajax_history/views_ajax_history.info.yml
@@ -1,13 +1,12 @@
-type: module
 name: 'Views AJAX History'
+type: module
 description: 'Add bookmarking abilities to AJAX Views.'
-# core: 8.x
+core_version_requirement: ^8.8 || ^9
 package: Views
 dependencies:
   - drupal:views
 
-# Information added by Drupal.org packaging script on 2019-07-05
-version: '8.x-1.2'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-04-29
+version: '8.x-1.5'
 project: 'views_ajax_history'
-datestamp: 1562339889
+datestamp: 1588147487
diff --git a/web/modules/views_ajax_history/views_ajax_history.install b/web/modules/views_ajax_history/views_ajax_history.install
new file mode 100644
index 0000000000000000000000000000000000000000..0ab5f592cfa225262e5970f5e744d2b3a00301f7
--- /dev/null
+++ b/web/modules/views_ajax_history/views_ajax_history.install
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Implements hook_install().
+ */
+function views_ajax_history_install() {
+  views_ajax_history_update_views_settings_handler();
+}
+
+/**
+ * Enable the ajax_history views display extender.
+ */
+function views_ajax_history_update_8001() {
+  views_ajax_history_update_views_settings_handler();
+
+  // Keep the previous behavior of the module by enabling the new 'ajax_history'
+  // display extender on every view that uses AJAX.
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('views.view.') as $view_config_name) {
+    $config = $config_factory->getEditable($view_config_name);
+    if (!$config->get('display')['default']['display_options']['use_ajax'] == TRUE) {
+      continue;
+    }
+
+    $save = FALSE;
+    foreach ($config->get('display') as $display_id => $display) {
+      if (!isset($display['display_options']['display_extenders']['ajax_history'])) {
+        $display['display_options']['display_extenders']['ajax_history']['enable_history'] = TRUE;
+        $config->set("display.$display_id", $display);
+        $save = TRUE;
+      }
+    }
+    if ($save) {
+      $config->save(TRUE);
+    }
+  }
+}
+
+/**
+ * Helper function for enable display extender for Views.
+ */
+function views_ajax_history_update_views_settings_handler() {
+  // Enable ajax_history plugin.
+  $config = \Drupal::service('config.factory')->getEditable('views.settings');
+  $display_extenders = $config->get('display_extenders') ?: [];
+  $display_extenders[] = 'ajax_history';
+  $config->set('display_extenders', $display_extenders);
+  $config->save();
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function views_ajax_history_uninstall() {
+  // Disable ajax_history plugin.
+  $config = \Drupal::service('config.factory')->getEditable('views.settings');
+  $display_extenders = $config->get('display_extenders') ?: [];
+  $key = array_search('ajax_history', $display_extenders);
+  if ($key !== FALSE) {
+    unset($display_extenders[$key]);
+    $config->set('display_extenders', $display_extenders);
+    $config->save();
+  }
+}
diff --git a/web/modules/views_ajax_history/views_ajax_history.module b/web/modules/views_ajax_history/views_ajax_history.module
index d335bdaea60e524636f6dc95cfc9a6f4d6d7925c..fe8fecb09afe5c2e8cb3d8a586abe3b3dfdc9662 100644
--- a/web/modules/views_ajax_history/views_ajax_history.module
+++ b/web/modules/views_ajax_history/views_ajax_history.module
@@ -6,15 +6,38 @@
  */
 
 use \Drupal\views\ViewExecutable;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function views_ajax_history_help($route_name, RouteMatchInterface $arg) {
+  switch ($route_name) {
+    case 'help.page.views_ajax_history':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Enable bookmaking of AJAX views. Supports filters and paging.') . '</p>';
+
+            // Add a link to the Drupal.org project.
+      $output .= '<p>';
+      $output .= t('Visit the <a href=":project_link">Views AJAX History project pages</a> on Drupal.org for more information.',[
+        ':project_link' => 'https://www.drupal.org/project/views_ajax_history'
+        ]);
+      $output .= '</p>';
+
+      return $output;
+  }
+}
 
 /**
  * Implements hook_views_pre_render().
  */
 function views_ajax_history_views_pre_render(ViewExecutable $view) {
-  if ($view->ajaxEnabled() && empty($view->is_attachment) && empty($view->live_preview)) {
+  $display_extenders_options = $view->display_handler->getOption('display_extenders');
+  if (($view->ajaxEnabled() && (isset($display_extenders_options['ajax_history']['enable_history']) && $display_extenders_options['ajax_history']['enable_history'] == TRUE)) && empty($view->is_attachment) && empty($view->live_preview)) {
     // @TODO add option to views form for html4+html5 or html5 only
     $view->element['#attached']['library'][] = 'views_ajax_history/history';
-    $view->element['#attached']['drupalSettings']['viewsAjaxHistory'] = ['renderPageItem' => pager_find_page()];
+    $view->element['#attached']['drupalSettings']['viewsAjaxHistory'] = ['renderPageItem' => \Drupal::service('pager.parameters')->findPage()];
     $view->element['#cache']['contexts'][] = 'url.query_args.pagers';
   }
 }
diff --git a/web/modules/views_bulk_operations/composer.json b/web/modules/views_bulk_operations/composer.json
index 1affe7a54380b3b494236e9edb68cbf49ec5ca88..9b8d29466d577b25869c2ad821008400bbba2fee 100644
--- a/web/modules/views_bulk_operations/composer.json
+++ b/web/modules/views_bulk_operations/composer.json
@@ -13,15 +13,21 @@
     "issues": "https://www.drupal.org/project/issues/views_bulk_operations?version=8.x",
     "docs": "https://www.drupal.org/docs/8/modules/views-bulk-operations-vbo"
   },
-  "license": "GPL-2.0+",
+  "license": "GPL-2.0-or-later",
   "minimum-stability": "dev",
   "require": {
-    "drupal/core": "~8.5"
+    "drupal/core": "^8.8 || ^9"
+  },
+  "require-dev": {
+    "drush/drush": "^10"
+  },
+  "suggest": {
+    "drush/drush": "^9 || ^10"
   },
   "extra": {
     "drush": {
       "services": {
-        "drush.services.yml": "^9"
+        "drush.services.yml": "^9 || ^10"
       }
     }
   }
diff --git a/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml b/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml
index febcb5480ce1f41946da96e65dd26040bfbb79c7..8feab21c14464bb9de384db9ca69395c024a79b0 100644
--- a/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml
+++ b/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml
@@ -18,11 +18,17 @@ views.field.views_bulk_operations_bulk_form:
       type: string
       label: 'Title of the action selector form element'
     selected_actions:
-      type: ignore
-      label: 'Selected actions array'
-    preconfiguration:
-      type: ignore
-      label: 'Preliminary configuration array'
+      type: sequence
+      label: 'Selected actions data array'
+      sequence:
+        type: mapping
+        mapping:
+          action_id:
+            type: string
+            label: 'Action plugin ID'
+          preconfiguration:
+            label: 'Preliminary configuration array for the plugin'
+            type: ignore
     clear_on_exposed:
       type: boolean
       label: 'Clear selection when exposed filters change'
diff --git a/web/modules/views_bulk_operations/js/adminUi.js b/web/modules/views_bulk_operations/js/adminUi.js
index 7a2b3f64dc56eb6d17a41be2892ccd94ba8c2355..e7dba636957d7deec58bbcfdc69fec1292ad99b2 100644
--- a/web/modules/views_bulk_operations/js/adminUi.js
+++ b/web/modules/views_bulk_operations/js/adminUi.js
@@ -22,22 +22,6 @@
   Drupal.viewsBulkOperationsUi = function () {
     var uiElement = $(this);
 
-    // Show / hide actions' preliminary configuration.
-    uiElement.find('.vbo-action-state').each(function () {
-      var matches = $(this).attr('name').match(/.*\[.*?\]\[(.*?)\]\[.*?\]/);
-      if (typeof (matches[1]) != 'undefined') {
-        var preconfigurationElement = uiElement.find('*[data-for="' + matches[1] + '"]');
-        $(this).change(function (event) {
-          if ($(this).is(':checked')) {
-            preconfigurationElement.show('fast');
-          }
-          else {
-            preconfigurationElement.hide('fast');
-          }
-        });
-      }
-    });
-
     // Select / deselect all functionality.
     var actionsElementWrapper = uiElement.find('details.vbo-actions-widget > .details-wrapper');
     if (actionsElementWrapper.length) {
diff --git a/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml
index aa8d26da2ab6d078948e35202be77a9bbc26d6d1..af36f021460ad69b14574eeb5079172aa4faa1d0 100644
--- a/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml
+++ b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml
@@ -2,11 +2,11 @@ type: module
 name: 'Actions Permissions'
 description: 'Adds access permissions on all actions allowing admins to restrict access on a per-role basis.'
 package: 'Views Bulk Operations'
-core: 8.x
+core_version_requirement: ^8 || ^9
 dependencies:
   - drupal:views_bulk_operations
 
-# Information added by Drupal.org packaging script on 2020-02-04
-version: '8.x-3.4'
+# Information added by Drupal.org packaging script on 2020-06-04
+version: '8.x-3.8'
 project: 'views_bulk_operations'
-datestamp: 1580807961
+datestamp: 1591296882
diff --git a/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml b/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml
index 9fca589a281800ca4b0d881f7f6c3a4ea520f37a..e8732eed7729178ebd8183876faa02da239b048f 100644
--- a/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml
+++ b/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml
@@ -2,11 +2,11 @@ type: module
 name: 'Views Bulk Operations example'
 description: 'Defines an example action with all possible options.'
 package: 'Examples'
-core: 8.x
+core_version_requirement: ^8 || ^9
 dependencies:
   - drupal:views_bulk_operations
 
-# Information added by Drupal.org packaging script on 2020-02-04
-version: '8.x-3.4'
+# Information added by Drupal.org packaging script on 2020-06-04
+version: '8.x-3.8'
 project: 'views_bulk_operations'
-datestamp: 1580807961
+datestamp: 1591296882
diff --git a/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php
index 6a1922a883aa3c6d345f138138e7509cff959a4a..3a37707aa82295f79e9f55c7d0c9a6fd277a2a09 100644
--- a/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php
+++ b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\views_bulk_operations\Action;
 
+use Drupal\Component\Plugin\ConfigurableInterface;
 use Drupal\Core\Action\ActionBase;
-use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\Core\Session\AccountInterface;
@@ -14,7 +14,7 @@
  * Provides a base implementation for a configurable
  * and preconfigurable VBO Action plugin.
  */
-abstract class ViewsBulkOperationsActionBase extends ActionBase implements ViewsBulkOperationsActionInterface, ConfigurablePluginInterface {
+abstract class ViewsBulkOperationsActionBase extends ActionBase implements ViewsBulkOperationsActionInterface, ConfigurableInterface {
 
   /**
    * Action context.
@@ -124,13 +124,6 @@ public function setConfiguration(array $configuration) {
     $this->configuration = $configuration;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function calculateDependencies() {
-    return [];
-  }
-
   /**
    * Default custom access callback.
    *
diff --git a/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php b/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php
index 95e72beb5b9436ec5152e817df54711b93eee163..8579de308f6f74c5a8a24262c8899417c25cb771 100644
--- a/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php
+++ b/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php
@@ -142,6 +142,7 @@ public function __construct(
     $this->tempStoreFactory = $tempStoreFactory;
     $this->currentUser = $currentUser;
     $this->requestStack = $requestStack;
+
   }
 
   /**
@@ -352,10 +353,9 @@ protected function defineOptions() {
     $options['batch_size'] = ['default' => 10];
     $options['form_step'] = ['default' => TRUE];
     $options['buttons'] = ['default' => FALSE];
-    $options['clear_on_exposed'] = ['default' => FALSE];
+    $options['clear_on_exposed'] = ['default' => TRUE];
     $options['action_title'] = ['default' => $this->t('Action')];
     $options['selected_actions'] = ['default' => []];
-    $options['preconfiguration'] = ['default' => []];
     return $options;
   }
 
@@ -411,7 +411,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
     $form['clear_on_exposed'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Clear selection when exposed filters change.'),
-      '#description' => $this->t('With this enabled, selection will be cleared everey time exposed filters are changed, select all will select all rows with exposed filters applied and view total count will take exposed filters into account. When disabled, select all selects all results in the view with empty exposed filters and one can change exposed filters while selecting rows without the selection being lost.'),
+      '#description' => $this->t('With this enabled, selection will be cleared every time exposed filters are changed, select all will select all rows with exposed filters applied and view total count will take exposed filters into account. When disabled, select all selects all results in the view with empty exposed filters and one can change exposed filters while selecting rows without the selection being lost.'),
       '#default_value' => $this->options['clear_on_exposed'],
     ];
 
@@ -433,56 +433,70 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
     // Load values for display.
     $form_values = $form_state->getValue(['options', 'selected_actions']);
     if (is_null($form_values)) {
-      $selected_actions = $this->options['selected_actions'];
-      $preconfiguration = $this->options['preconfiguration'];
+      $config_data = $this->options['selected_actions'];
+      $selected_actions_data = [];
+      foreach ($config_data as $key => $item) {
+        $selected_actions_data[$item['action_id']] = $item;
+      }
     }
     else {
-      $selected_actions = [];
-      $preconfiguration = [];
-      foreach ($form_values as $id => $value) {
-        $selected_actions[$id] = $value['state'] ? $id : 0;
-        $preconfiguration[$id] = isset($value['preconfiguration']) ? $value['preconfiguration'] : [];
-      }
+      $selected_actions_data = $form_values;
     }
 
+    $delta = 0;
     foreach ($this->actions as $id => $action) {
-      $form['selected_actions'][$id]['state'] = [
+      $form['selected_actions'][$delta]['action_id'] = [
+        '#type' => 'value',
+        '#value' => $id,
+      ];
+      $form['selected_actions'][$delta]['state'] = [
         '#type' => 'checkbox',
         '#title' => $action['label'],
-        '#default_value' => empty($selected_actions[$id]) ? 0 : 1,
+        '#default_value' => empty($selected_actions_data[$id]) ? 0 : 1,
         '#attributes' => ['class' => ['vbo-action-state']],
       ];
 
       // There are problems with AJAX on this form when adding
       // new elements (Views issue), a workaround is to render
       // all elements and show/hide them when needed.
-      $form['selected_actions'][$id]['preconfiguration'] = [
+      $form['selected_actions'][$delta]['preconfiguration'] = [
         '#type' => 'fieldset',
         '#title' => $this->t('Preconfiguration for "@action"', [
           '@action' => $action['label'],
         ]),
-        '#attributes' => [
-          'data-for' => $id,
-          'style' => empty($selected_actions[$id]) ? 'display: none' : NULL,
+        '#states' => [
+          'visible' => [
+            sprintf('[name="options[selected_actions][%d][state]"]', $delta) => ['checked' => TRUE],
+          ],
         ],
       ];
 
       // Default label_override element.
-      $form['selected_actions'][$id]['preconfiguration']['label_override'] = [
+      $form['selected_actions'][$delta]['preconfiguration']['label_override'] = [
         '#type' => 'textfield',
         '#title' => $this->t('Override label'),
         '#description' => $this->t('Leave empty for the default label.'),
-        '#default_value' => isset($preconfiguration[$id]['label_override']) ? $preconfiguration[$id]['label_override'] : '',
+        '#default_value' => isset($selected_actions_data[$id]['preconfiguration']['label_override']) ? $selected_actions_data[$id]['preconfiguration']['label_override'] : '',
       ];
 
       // Load preconfiguration form if available.
       if (method_exists($action['class'], 'buildPreConfigurationForm')) {
-        if (!isset($preconfiguration[$id])) {
-          $preconfiguration[$id] = [];
+        if (!isset($selected_actions_data[$id]['preconfiguration'])) {
+          $selected_actions_data[$id]['preconfiguration'] = [];
         }
         $actionObject = $this->actionManager->createInstance($id);
-        $form['selected_actions'][$id]['preconfiguration'] = $actionObject->buildPreConfigurationForm($form['selected_actions'][$id]['preconfiguration'], $preconfiguration[$id], $form_state);
+
+        // Set the view so the configuration form can access to it.
+        if ($this->view instanceof ViewExecutable) {
+          if ($this->view->inited !== TRUE) {
+            $this->view->initHandlers();
+          }
+          $actionObject->setView($this->view);
+        }
+        $form['selected_actions'][$delta]['preconfiguration'] = $actionObject->buildPreConfigurationForm($form['selected_actions'][$delta]['preconfiguration'], $selected_actions_data[$id]['preconfiguration'], $form_state);
       }
+
+      $delta++;
     }
 
     parent::buildOptionsForm($form, $form_state);
@@ -492,18 +506,17 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
-    $options = &$form_state->getValue('options');
-    foreach ($options['selected_actions'] as $id => $action) {
-      if (!empty($action['state'])) {
-        if (isset($action['preconfiguration'])) {
-          $options['preconfiguration'][$id] = $action['preconfiguration'];
-          unset($options['selected_actions'][$id]['preconfiguration']);
-        }
-        $options['selected_actions'][$id] = $id;
+    $selected_actions = &$form_state->getValue(['options', 'selected_actions']);
+    $selected_actions = array_filter($selected_actions, function ($action_data) {
+      return !empty($action_data['state']);
+    });
+    foreach ($selected_actions as $delta => &$item) {
+      unset($item['state']);
+      if (empty($item['preconfiguration']['label_override'])) {
+        unset($item['preconfiguration']['label_override']);
       }
-      else {
-        unset($options['preconfiguration'][$id]);
-        $options['selected_actions'][$id] = 0;
+      if (empty($item['preconfiguration'])) {
+        unset($item['preconfiguration']);
       }
     }
     parent::submitOptionsForm($form, $form_state);
@@ -767,12 +780,13 @@ public static function viewsFormAjax(array $form, FormStateInterface $form_state
   protected function getBulkOptions() {
     if (!isset($this->bulkOptions)) {
       $this->bulkOptions = [];
-      foreach ($this->actions as $id => $definition) {
-        // Filter out actions that weren't selected.
-        if (!in_array($id, $this->options['selected_actions'], TRUE)) {
+      foreach ($this->options['selected_actions'] as $key => $selected_action_data) {
+        if (!isset($this->actions[$selected_action_data['action_id']])) {
           continue;
         }
 
+        $definition = $this->actions[$selected_action_data['action_id']];
+
         // Check access permission, if defined.
         if (!empty($definition['requirements']['_permission']) && !$this->currentUser->hasPermission($definition['requirements']['_permission'])) {
           continue;
@@ -784,11 +798,11 @@ protected function getBulkOptions() {
         }
 
         // Override label if applicable.
-        if (!empty($this->options['preconfiguration'][$id]['label_override'])) {
-          $this->bulkOptions[$id] = $this->options['preconfiguration'][$id]['label_override'];
+        if (!empty($selected_action_data['preconfiguration']['label_override'])) {
+          $this->bulkOptions[$key] = $selected_action_data['preconfiguration']['label_override'];
         }
         else {
-          $this->bulkOptions[$id] = $definition['label'];
+          $this->bulkOptions[$key] = $definition['label'];
         }
       }
     }
@@ -807,14 +821,14 @@ protected function getBulkOptions() {
   public function viewsFormSubmit(array &$form, FormStateInterface $form_state) {
     if ($form_state->get('step') == 'views_form_views_form') {
 
-      $action_id = $form_state->getValue('action');
+      $action_config = $this->options['selected_actions'][$form_state->getValue('action')];
 
-      $action = $this->actions[$action_id];
+      $action = $this->actions[$action_config['action_id']];
 
-      $this->tempStoreData['action_id'] = $action_id;
-      $this->tempStoreData['action_label'] = empty($this->options['preconfiguration'][$action_id]['label_override']) ? (string) $action['label'] : $this->options['preconfiguration'][$action_id]['label_override'];
+      $this->tempStoreData['action_id'] = $action_config['action_id'];
+      $this->tempStoreData['action_label'] = empty($action_config['preconfiguration']['label_override']) ? (string) $action['label'] : $action_config['preconfiguration']['label_override'];
       $this->tempStoreData['relationship_id'] = $this->options['relationship'];
-      $this->tempStoreData['preconfiguration'] = isset($this->options['preconfiguration'][$action_id]) ? $this->options['preconfiguration'][$action_id] : [];
+      $this->tempStoreData['preconfiguration'] = isset($action_config['preconfiguration']) ? $action_config['preconfiguration'] : [];
       $this->tempStoreData['clear_on_exposed'] = $this->options['clear_on_exposed'];
 
       $configurable = $this->isActionConfigurable($action);
@@ -918,17 +932,23 @@ public function clearSelection(array &$form, FormStateInterface $form_state) {
   public function viewsFormValidate(&$form, FormStateInterface $form_state) {
     if ($this->options['buttons']) {
       $trigger = $form_state->getTriggeringElement();
-      $action_id = end($trigger['#parents']);
-      $form_state->setValue('action', $action_id);
+      $action_delta = end($trigger['#parents']);
+      $form_state->setValue('action', $action_delta);
+    }
+    else {
+      $action_delta = $form_state->getValue('action');
     }
 
-    if (empty($form_state->getValue('action'))) {
+    if ($action_delta === '') {
       $form_state->setErrorByName('action', $this->t('Please select an action to perform.'));
     }
-
-    // This happened once, can't reproduce but here's a safety switch.
-    if (!isset($this->actions[$form_state->getValue('action')])) {
-      $form_state->setErrorByName('action', $this->t('Form error occurred, please try again.'));
+    else {
+      if (!isset($this->options['selected_actions'][$action_delta])) {
+        $form_state->setErrorByName('action', $this->t('Form error occurred, please try again.'));
+      }
+      elseif (!isset($this->actions[$this->options['selected_actions'][$action_delta]['action_id']])) {
+        $form_state->setErrorByName('action', $this->t('Form error occurred, Unavailable action selected.'));
+      }
     }
 
     if (!$form_state->getValue('select_all')) {
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php
index 4c60e104894d1daa38744ffcf0f5938d228add49..40af081bb9af06ef4ca7d1137af36110e22ddcc6 100644
--- a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php
@@ -65,7 +65,10 @@ protected function findDefinitions() {
 
     // Incompatible actions.
     $incompatible = [
+      // Deprecated anyway, to be deleted eventually.
       'node_delete_action',
+      // Those are up to date.
+      'entity:delete_action:node',
       'user_cancel_user_action',
     ];
 
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php
index 481ee4c7814062b99f6b2d4d28a8b1dbf98e234f..9ecc1f9eca6a4474608380956b2deb66eae6717f 100644
--- a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php
@@ -210,6 +210,26 @@ public function getPageList($page) {
       $this->view->setExposedInput(['_views_bulk_operations_override' => TRUE]);
     }
 
+    // In some cases we may encounter nondeterministic bahaviour in
+    // db queries with sorts allowing different order of results.
+    // To fix this we're removing all sorts and setting one sorting
+    // rule by the view base id field.
+    $sorts = $this->view->getHandlers('sort');
+    foreach ($sorts as $id => $sort) {
+      $this->view->setHandler($this->bulkFormData['display_id'], 'sort', $id, NULL);
+    }
+    $base_field = $this->view->storage->get('base_field');
+    $this->view->setHandler($this->bulkFormData['display_id'], 'sort', $base_field, [
+      'id' => $base_field,
+      'table' => $this->view->storage->get('base_table'),
+      'field' => $base_field,
+      'order' => 'ASC',
+      'relationship' => 'none',
+      'group_type' => 'group',
+      'exposed' => 'FALSE',
+      'plugin_id' => 'standard',
+    ]);
+
     $this->view->setItemsPerPage($this->bulkFormData['batch_size']);
     $this->view->setCurrentPage($page);
     $this->view->build();
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php
index f9f69b43ef9f74cbd452921bd0e8a5b60e271079..d72ae49e0eddec0be7e605e254418708d527c015 100644
--- a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php
@@ -3,6 +3,7 @@
 namespace Drupal\views_bulk_operations\Service;
 
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Drupal\Core\Pager\PagerManagerInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\Views;
@@ -22,6 +23,13 @@ class ViewsBulkOperationsViewData implements ViewsBulkOperationsViewDataInterfac
    */
   protected $eventDispatcher;
 
+  /**
+   * Pager manager service.
+   *
+   * @var \Drupal\Core\Pager\PagerManagerInterface
+   */
+  protected $pagerManager;
+
   /**
    * The current view.
    *
@@ -62,9 +70,15 @@ class ViewsBulkOperationsViewData implements ViewsBulkOperationsViewDataInterfac
    *
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
    *   The event dispatcher service.
+   * @param \Drupal\Core\Pager\PagerManagerInterface $pagerManager
+   *   Pager manager service.
    */
-  public function __construct(EventDispatcherInterface $eventDispatcher) {
+  public function __construct(
+    EventDispatcherInterface $eventDispatcher,
+    PagerManagerInterface $pagerManager
+  ) {
     $this->eventDispatcher = $eventDispatcher;
+    $this->pagerManager = $pagerManager;
   }
 
   /**
@@ -159,30 +173,6 @@ public function getEntity(ResultRow $row) {
     }
   }
 
-  /**
-   * Helper function that restores pager data.
-   *
-   * Pager data is stored in global variables and changed every
-   * time the view is executed, even if in a new object instance
-   * so we need to save and restore the original values.
-   */
-  protected function fixPagerData() {
-    static $values;
-    if (!isset($values)) {
-      foreach (['pager_page_array', 'pager_total', 'pager_total_items'] as $key) {
-        if (isset($GLOBALS[$key])) {
-          $values[$key] = $GLOBALS[$key];
-        }
-      }
-    }
-    elseif (!empty($values)) {
-      foreach ($values as $key => $value) {
-        $GLOBALS[$key] = $value;
-      }
-      unset($values);
-    }
-  }
-
   /**
    * Get the total count of results on all pages.
    *
@@ -196,6 +186,11 @@ public function getTotalResults($clear_on_exposed = FALSE) {
     $total_results = NULL;
 
     if (!$clear_on_exposed && !empty($this->view->getExposedInput())) {
+      if ($pager = $this->view->getPager()) {
+        $pager_options = $pager->options;
+        $pager_options['total_items'] = $pager->getTotalItems();
+      }
+
       // Execute the view without exposed input set.
       $view = Views::getView($this->view->id());
       $view->setDisplay($this->view->current_display);
@@ -208,19 +203,16 @@ public function getTotalResults($clear_on_exposed = FALSE) {
       // We have to set exposed input to some value here, empty
       // value will be overwritten with query params by Views so
       // setting an empty array wouldn't work.
+      $pager = $view->getPager();
       $view->setExposedInput(['_views_bulk_operations_override' => TRUE]);
     }
     else {
       $view = $this->view;
     }
 
-    $this->fixPagerData();
-
     // Execute the view if not already executed.
     $view->execute();
 
-    $this->fixPagerData();
-
     if (!empty($view->pager->total_items)) {
       $total_results = $view->pager->total_items;
     }
@@ -228,6 +220,10 @@ public function getTotalResults($clear_on_exposed = FALSE) {
       $total_results = $view->total_rows;
     }
 
+    if (!empty($pager_options)) {
+      $this->pagerManager->createPager($pager_options['total_items'], $pager_options['items_per_page'], $pager_options['id']);
+    }
+
     return $total_results;
   }
 
diff --git a/web/modules/views_bulk_operations/tests/src/Functional/DrushCommandsTest.php b/web/modules/views_bulk_operations/tests/src/Functional/DrushCommandsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..91a86b3b59ef8bb6b57e950609e9e113f09d13f1
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Functional/DrushCommandsTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Drush\TestTraits\DrushTestTrait;
+
+/**
+ * @coversDefaultClass \Drupal\views_bulk_operations\Commands\ViewsBulkOperationsCommands
+ * @group views_bulk_operations
+ */
+class DrushCommandsTest extends BrowserTestBase {
+  use DrushTestTrait;
+
+  const TEST_NODE_COUNT = 15;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'views',
+    'views_bulk_operations',
+    'views_bulk_operations_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create some nodes for testing.
+    $this->drupalCreateContentType(['type' => 'page']);
+
+    $this->testNodes = [];
+    $time = $this->container->get('datetime.time')->getRequestTime();
+    for ($i = 0; $i < self::TEST_NODE_COUNT; $i++) {
+      // Ensure nodes are sorted in the same order they are inserted in the
+      // array.
+      $time -= $i;
+      $this->testNodes[] = $this->drupalCreateNode([
+        'type' => 'page',
+        'title' => 'Title ' . $i,
+        'sticky' => FALSE,
+        'created' => $time,
+        'changed' => $time,
+      ]);
+    }
+
+  }
+
+  /**
+   * Tests the VBO Drush command.
+   */
+  public function testDrushCommand() {
+    $this->drush('vbo-exec', ['views_bulk_operations_test', 'views_bulk_operations_simple_test_action']);
+    $this->assertStringContainsString('Test action (preconfig: , label: Title 0)', $this->getErrorOutput());
+    $this->assertStringContainsString('Test action (preconfig: , label: Title 14)', $this->getErrorOutput());
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php b/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php
index 0a83927e30606700907da4e5301e4163b038db00..b008520b345e89065ee653f413b89622a363b430 100644
--- a/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php
+++ b/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php
@@ -13,6 +13,11 @@ class ViewsBulkOperationsBulkFormTest extends BrowserTestBase {
 
   const TEST_NODE_COUNT = 15;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stable';
+
   /**
    * Modules to install.
    *
@@ -103,7 +108,7 @@ public function testViewsBulkOperationsBulkFormSimple() {
 
     // Test that the views edit header appears first.
     $first_form_element = $this->xpath('//form/div[1][@id = :id]', [':id' => 'edit-header']);
-    $this->assertTrue($first_form_element, 'The views form edit header appears first.');
+    $this->assertNotEmpty($first_form_element, 'The views form edit header appears first.');
 
     // Make sure a checkbox appears on all rows.
     $edit = [];
@@ -112,7 +117,7 @@ public function testViewsBulkOperationsBulkFormSimple() {
     }
 
     // The advanced action should not be shown on the form - no permission.
-    $this->assertTrue(empty($this->cssSelect('select[name=views_bulk_operations_advanced_test_action]')), t('Advanced action is not selectable.'));
+    $this->assertEmpty($this->cssSelect('input[value=views_bulk_operations_advanced_test_action]'), t('Advanced action is not selectable.'));
 
     // Log in as a user with 'edit any page content' permission
     // to have access to perform the test operation.
@@ -131,7 +136,7 @@ public function testViewsBulkOperationsBulkFormSimple() {
 
     $testViewConfig = \Drupal::service('config.factory')->get('views.view.views_bulk_operations_test');
     $configData = $testViewConfig->getRawData();
-    $preconfig_setting = $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['preconfiguration']['views_bulk_operations_simple_test_action']['preconfig'];
+    $preconfig_setting = $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['selected_actions'][0]['preconfiguration']['preconfig'];
 
     foreach ($selected as $index) {
       $assertSession->pageTextContains(
@@ -182,7 +187,7 @@ public function testViewsBulkOperationsBulkFormAdvanced() {
     // First execute the simple action to test
     // the ViewsBulkOperationsController class.
     $edit = [
-      'action' => 'views_bulk_operations_simple_test_action',
+      'action' => 0,
     ];
     $selected = [0, 2];
     foreach ($selected as $index) {
@@ -197,7 +202,7 @@ public function testViewsBulkOperationsBulkFormAdvanced() {
 
     // Execute the advanced test action.
     $edit = [
-      'action' => 'views_bulk_operations_advanced_test_action',
+      'action' => 1,
     ];
     $selected = [0, 1, 3];
     foreach ($selected as $index) {
@@ -230,7 +235,7 @@ public function testViewsBulkOperationsBulkFormAdvanced() {
     // the next page should display results.
     $testViewConfig = \Drupal::service('config.factory')->get('views.view.views_bulk_operations_test_advanced');
     $configData = $testViewConfig->getRawData();
-    $preconfig_setting = $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['preconfiguration']['views_bulk_operations_advanced_test_action']['test_preconfig'];
+    $preconfig_setting = $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['selected_actions'][1]['preconfiguration']['test_preconfig'];
 
     // NOTE: The view pager has an offset set on this view, so checkbox
     // indexes are not equal to test nodes array keys. Hence the $index + 1.
@@ -245,7 +250,7 @@ public function testViewsBulkOperationsBulkFormAdvanced() {
     // Test the exclude functionality with batching and entity
     // property changes affecting view query results.
     $edit = [
-      'action' => 'views_bulk_operations_advanced_test_action',
+      'action' => 1,
       'select_all' => 1,
     ];
     // Let's leave two checkboxes unchecked to test the exclude mode.
@@ -261,7 +266,8 @@ public function testViewsBulkOperationsBulkFormAdvanced() {
       sprintf('Action processing results: Test (%d).', (count($this->testNodes) - 3)),
       sprintf('Action has been executed on all %d nodes.', (count($this->testNodes) - 3))
     );
-    $this->assertTrue((count($this->cssSelect('table.views-table tbody tr')) === 2), t("The view shows only excluded results."));
+
+    $this->assertNotEmpty((count($this->cssSelect('table.vbo-table tbody tr')) === 2), "The view shows only excluded results.");
   }
 
   /**
@@ -302,7 +308,7 @@ public function testViewsBulkOperationsBulkFormPassing() {
 
       // Populate form values.
       $edit = [
-        'action' => 'views_bulk_operations_passing_test_action',
+        'action' => 2,
       ];
       if ($case['selection']) {
         foreach ($selected as $index) {
diff --git a/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php
index 66b996944329e45ba43b2b494114080a66282540..7f250106df2c24454288d76dd6c155d68d447739 100644
--- a/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php
+++ b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php
@@ -2,8 +2,6 @@
 
 namespace Drupal\Tests\views_bulk_operations\Kernel;
 
-use Drupal\node\NodeInterface;
-
 /**
  * @coversDefaultClass \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessor
  * @group views_bulk_operations
@@ -34,19 +32,14 @@ public function setUp() {
   protected function assertNodeStatuses(array $list, $exclude = FALSE) {
     $nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
 
-    $expected = [
-      $exclude ? NodeInterface::PUBLISHED : NodeInterface::NOT_PUBLISHED,
-      $exclude ? NodeInterface::NOT_PUBLISHED : NodeInterface::PUBLISHED,
-    ];
-
     $statuses = [];
 
     foreach ($this->testNodesData as $id => $lang_data) {
       $node = $nodeStorage->load($id);
-      $statuses[$id] = intval($node->status->value);
+      $statuses[$id] = $node->isPublished();
 
       // Reset node status.
-      $node->status->value = 1;
+      $node->setPublished();
       $node->save();
     }
 
@@ -54,13 +47,13 @@ protected function assertNodeStatuses(array $list, $exclude = FALSE) {
       $asserted = FALSE;
       foreach ($list as $item) {
         if ($item[3] == $id) {
-          $this->assertEquals($expected[0], $status);
+          $this->assertEquals((bool) $exclude, $status);
           $asserted = TRUE;
           break;
         }
       }
       if (!$asserted) {
-        $this->assertEquals($expected[1], $status);
+        $this->assertEquals(!(bool) $exclude, $status);
       }
     }
   }
diff --git a/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php b/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php
index 286a4c42a5b3204cae10b7b70f6fbea95caf0c2e..3ba5a30b802a27a6f0ef8629262a4385fb2aa91a 100644
--- a/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php
+++ b/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php
@@ -94,12 +94,12 @@ public function testOperation() {
       ->with('test_view')
       ->will($this->returnValue($view));
 
-    $entity_manager = $this->createMock('Drupal\Core\Entity\EntityManagerInterface');
-    $entity_manager->expects($this->any())
+    $entity_type_manager = $this->createMock('Drupal\Core\Entity\EntityTypeManagerInterface');
+    $entity_type_manager->expects($this->any())
       ->method('getStorage')
       ->with('view')
       ->will($this->returnValue($view_storage));
-    $this->container->set('entity.manager', $entity_manager);
+    $this->container->set('entity_type.manager', $entity_type_manager);
 
     $executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
       ->disableOriginalConstructor()
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml
index 9a2d0057ff8432655c8d677883ca9849a2abefa1..712b47789d56160130b09f7f54de64ffa8baeb40 100644
--- a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml
@@ -170,15 +170,15 @@ display:
           buttons: true
           action_title: Action
           selected_actions:
-            views_bulk_operations_simple_test_action: views_bulk_operations_simple_test_action
-            views_bulk_operations_advanced_test_action: views_bulk_operations_advanced_test_action
-          preconfiguration:
-            views_bulk_operations_simple_test_action:
-              label_override: 'Simple test action'
-              preconfig: 'Test setting'
-            views_bulk_operations_advanced_test_action:
-              label_override: ''
-              preconfig: 'Test setting'
+            0:
+              action_id: views_bulk_operations_simple_test_action
+              preconfiguration:
+                label_override: 'Simple test action'
+                preconfig: 'Test setting'
+            1:
+              action_id: views_bulk_operations_advanced_test_action
+              preconfiguration:
+                preconfig: 'Test setting'
           plugin_id: views_bulk_operations_bulk_form
       filters: {  }
       sorts:
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml
index 67805a381b99262d1edd2784cc3673ac7a14b74d..9138c931b85c6150b49fb0374e743100e594c7df 100644
--- a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml
@@ -167,18 +167,17 @@ display:
           buttons: false
           action_title: Action
           selected_actions:
-            views_bulk_operations_simple_test_action: views_bulk_operations_simple_test_action
-            views_bulk_operations_advanced_test_action: views_bulk_operations_advanced_test_action
-            views_bulk_operations_passing_test_action: views_bulk_operations_passing_test_action
-          preconfiguration:
-            views_bulk_operations_simple_test_action:
-              label_override: 'Simple test action'
-              preconfig: 'Test setting'
-            views_bulk_operations_advanced_test_action:
-              label_override: ''
-              test_preconfig: 'Test setting'
-            views_bulk_operations_passing_test_action:
-              label_override: ''
+            0:
+              action_id: views_bulk_operations_simple_test_action
+              preconfiguration:
+                label_override: 'Simple test action'
+                preconfig: 'Test setting'
+            1:
+              action_id: views_bulk_operations_advanced_test_action
+              preconfiguration:
+                test_preconfig: 'Test setting'
+            2:
+              action_id: views_bulk_operations_passing_test_action
           plugin_id: views_bulk_operations_bulk_form
       filters:
         status:
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php
index 47bf354995442f81cbda37e41308333f02b982ec..ef16c633ced18a4dd2c2d074627778ad6cb6b905 100644
--- a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php
@@ -51,7 +51,7 @@ public function execute($entity = NULL) {
       if (!$entity->isDefaultTranslation()) {
         $entity = \Drupal::service('entity_type.manager')->getStorage('node')->load($entity->id());
       }
-      $entity->setPublished(FALSE);
+      $entity->setUnpublished();
       $entity->save();
     }
 
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml
index dbbc32fae922f20ffc93373178023359bcaad1bb..afb2114319ae056348b0c44c45a40b5ef56f6b23 100644
--- a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml
@@ -2,12 +2,12 @@ name: 'Views Bulk Operations test'
 type: module
 description: 'Support module for testing Views Bulk Operations.'
 package: Testing
-core: 8.x
+core_version_requirement: ^8.8 || ^9
 dependencies:
   - drupal:views_bulk_operations
   - drupal:node
 
-# Information added by Drupal.org packaging script on 2020-02-04
-version: '8.x-3.4'
+# Information added by Drupal.org packaging script on 2020-06-04
+version: '8.x-3.8'
 project: 'views_bulk_operations'
-datestamp: 1580807961
+datestamp: 1591296882
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.info.yml b/web/modules/views_bulk_operations/views_bulk_operations.info.yml
index 7f895d1016476037b96cd4e8bd8cbdc7b95feadf..e1dd4b9dd52e7fca31ecd18cb35cd58ae3182599 100644
--- a/web/modules/views_bulk_operations/views_bulk_operations.info.yml
+++ b/web/modules/views_bulk_operations/views_bulk_operations.info.yml
@@ -2,11 +2,11 @@ type: module
 name: 'Views Bulk Operations'
 description: 'Adds an ability to perform bulk operations on selected entities from view results.'
 package: 'Views'
-core: 8.x
+core_version_requirement: ^8.8 || ^9
 dependencies:
-  - drupal:views (>=8.5)
+  - drupal:views
 
-# Information added by Drupal.org packaging script on 2020-02-04
-version: '8.x-3.4'
+# Information added by Drupal.org packaging script on 2020-06-04
+version: '8.x-3.8'
 project: 'views_bulk_operations'
-datestamp: 1580807961
+datestamp: 1591296882
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.install b/web/modules/views_bulk_operations/views_bulk_operations.install
new file mode 100644
index 0000000000000000000000000000000000000000..f044c571703d241e1a5318e43f43815d6c3f17ab
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.install
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains update procedures for the module.
+ */
+
+/**
+ * Convert configuration of existing views to the new schema.
+ */
+function views_bulk_operations_update_8034(&$sandbox) {
+  $viewsStorage = \Drupal::service('entity_type.manager')->getStorage('view');
+
+  if (!isset($sandbox['current'])) {
+    $sandbox['total'] = $viewsStorage->getQuery()->count()->execute();
+    $sandbox['current'] = 0;
+    $sandbox['converted'] = 0;
+  }
+
+  $query = $viewsStorage->getQuery();
+
+  // Process 10 view configs at a time.
+  $query->range($sandbox['current'], 10);
+  $results = $query->execute();
+  if (!empty($results)) {
+    foreach ($results as $view_id) {
+      $view = $viewsStorage->load($view_id);
+      $displays = $view->get('display');
+      $converted = FALSE;
+
+      foreach ($displays as $display_id => &$display) {
+        if (!empty($display['display_options']['fields'])) {
+          foreach ($display['display_options']['fields'] as $field_id => &$field) {
+            if ($field['plugin_id'] === 'views_bulk_operations_bulk_form') {
+              $new_selected_actions = [];
+              foreach ($field['selected_actions'] as $plugin_id) {
+                if (!$plugin_id) {
+                  continue;
+                }
+                $action_config_array = ['action_id' => $plugin_id];
+                if (isset($field['preconfiguration']) && isset($field['preconfiguration'][$plugin_id])) {
+                  $action_config_array['preconfiguration'] = $field['preconfiguration'][$plugin_id];
+                }
+                $new_selected_actions[] = $action_config_array;
+              }
+              $field['selected_actions'] = $new_selected_actions;
+              unset($field['preconfiguration']);
+              $converted = TRUE;
+            }
+          }
+        }
+      }
+
+      if ($converted) {
+        $view->set('display', $displays);
+        $view->save();
+        $sandbox['converted']++;
+      }
+
+      $sandbox['current']++;
+      $sandbox['#finished'] = $sandbox['current'] / $sandbox['total'];
+
+    }
+  }
+
+  if ($sandbox['#finished'] >= 1) {
+    if ($sandbox['converted']) {
+      return t('@count view configs converted.', ['@count' => $sandbox['converted']]);
+    }
+    else {
+      return t('No conversions were required by Views Bulk Operations.');
+    }
+  }
+}
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.services.yml b/web/modules/views_bulk_operations/views_bulk_operations.services.yml
index 56e68de2b0a254f1554a07ebf1756f1222b2cca6..ef64d9e476501b7c1a8bcd8589255fb65f2d2858 100644
--- a/web/modules/views_bulk_operations/views_bulk_operations.services.yml
+++ b/web/modules/views_bulk_operations/views_bulk_operations.services.yml
@@ -1,7 +1,7 @@
 services:
   views_bulk_operations.data:
     class: Drupal\views_bulk_operations\Service\ViewsBulkOperationsViewData
-    arguments: ['@event_dispatcher']
+    arguments: ['@event_dispatcher', '@pager.manager']
   views_bulk_operations.processor:
     class: Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessor
     arguments: ['@views_bulk_operations.data', '@plugin.manager.views_bulk_operations_action', '@current_user', '@module_handler']