diff --git a/composer.json b/composer.json
index 1a392f6d643da2540665116d6f5efcc47e43f586..4c099b5ea7921ba48556fd4e6580fb862d7dfd99 100644
--- a/composer.json
+++ b/composer.json
@@ -88,7 +88,7 @@
         "drupal-composer/drupal-scaffold": "2.5.4",
         "drupal/address": "1.1",
         "drupal/addtocalendar": "3.1",
-        "drupal/admin_toolbar": "2.0",
+        "drupal/admin_toolbar": "2.2",
         "drupal/administerusersbyrole": "2.0-beta1",
         "drupal/allowed_formats": "1.2",
         "drupal/anchor_link": "1.6",
@@ -117,7 +117,7 @@
         "drupal/entity_browser": "1.4",
         "drupal/entity_clone": "1.0.0-beta3",
         "drupal/entity_embed": "1.0-beta2",
-        "drupal/entity_reference_revisions": "1.3",
+        "drupal/entity_reference_revisions": "1.8",
         "drupal/externalauth": "1.1",
         "drupal/features": "3.8",
         "drupal/field_group": "3.0",
@@ -127,7 +127,7 @@
         "drupal/geolocation": "1.10",
         "drupal/google_analytics": "2.4",
         "drupal/google_tag": "1.3",
-        "drupal/honeypot": "1.28",
+        "drupal/honeypot": "1.30",
         "drupal/image_popup": "1.1",
         "drupal/inline_entity_form": "1.0-rc1",
         "drupal/libraries": "3.0.0-alpha1",
@@ -136,7 +136,7 @@
         "drupal/magnific_popup": "1.3",
         "drupal/mathjax": "2.7",
         "drupal/media_entity_browser": "2.0-alpha2",
-        "drupal/media_entity_twitter": "2.0-alpha2",
+        "drupal/media_entity_twitter": "2.3",
         "drupal/menu_block": "1.4",
         "drupal/menu_block_title": "1.1",
         "drupal/menu_breadcrumb": "1.12",
@@ -318,4 +318,4 @@
             "php": "7.0.8"
         }
     }
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index fd58f4396839813c4a9dd5201d7ab9e636712b47..6e8ee99d0c75a54d4341f30f94dbf443417018b0 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": "f3f9a51e6c09cc6554fa012dbf1ad64d",
+    "content-hash": "e4746447f1d486ac63e393e17d8faba2",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -2113,20 +2113,20 @@
         },
         {
             "name": "drupal/admin_toolbar",
-            "version": "2.0.0",
+            "version": "2.2.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/admin_toolbar.git",
-                "reference": "8.x-2.0"
+                "reference": "8.x-2.2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.0.zip",
-                "reference": "8.x-2.0",
-                "shasum": "568de63dbaa8046a82d327dbd0b892ab79fb87aa"
+                "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.2.zip",
+                "reference": "8.x-2.2",
+                "shasum": "41ea0e3321e6d1e190c486be49a99e60446d8dd7"
             },
             "require": {
-                "drupal/core": "*"
+                "drupal/core": "^8.8.0 || ^9.0"
             },
             "type": "drupal-module",
             "extra": {
@@ -2134,8 +2134,8 @@
                     "dev-2.x": "2.x-dev"
                 },
                 "drupal": {
-                    "version": "8.x-2.0",
-                    "datestamp": "1573751237",
+                    "version": "8.x-2.2",
+                    "datestamp": "1585017179",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -4612,23 +4612,23 @@
         },
         {
             "name": "drupal/entity_reference_revisions",
-            "version": "1.3.0",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/entity_reference_revisions.git",
-                "reference": "8.x-1.3"
+                "reference": "8.x-1.8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.3.zip",
-                "reference": "8.x-1.3",
-                "shasum": "78aebb58efbbfcbb2faa40a1afc0830312b32631"
+                "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.8.zip",
+                "reference": "8.x-1.8",
+                "shasum": "c1279e6c683edc2dbccedba8de1505340c8a62b6"
             },
             "require": {
-                "drupal/core": "~8.0"
+                "drupal/core": "^8.7.7 || ^9"
             },
             "require-dev": {
-                "drupal/diff": "*"
+                "drupal/diff": "1.x-dev"
             },
             "type": "drupal-module",
             "extra": {
@@ -4636,8 +4636,8 @@
                     "dev-1.x": "1.x-dev"
                 },
                 "drupal": {
-                    "version": "8.x-1.3",
-                    "datestamp": "1515143885",
+                    "version": "8.x-1.8",
+                    "datestamp": "1583961846",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -4646,9 +4646,13 @@
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
             "license": [
-                "GPL-2.0-or-later"
+                "GPL-2.0"
             ],
             "authors": [
+                {
+                    "name": "Berdir",
+                    "homepage": "https://www.drupal.org/user/214652"
+                },
                 {
                     "name": "Frans",
                     "homepage": "https://www.drupal.org/user/514222"
@@ -4662,10 +4666,10 @@
                     "homepage": "https://www.drupal.org/user/227761"
                 }
             ],
-            "description": "Adds a Entity Reference field type with revision support.",
+            "description": "Entity Reference Revisions",
             "homepage": "https://www.drupal.org/project/entity_reference_revisions",
             "support": {
-                "source": "http://cgit.drupalcode.org/entity_reference_revisions"
+                "source": "https://git.drupalcode.org/project/entity_reference_revisions"
             }
         },
         {
@@ -5187,17 +5191,17 @@
         },
         {
             "name": "drupal/honeypot",
-            "version": "1.28.0",
+            "version": "1.30.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/honeypot.git",
-                "reference": "8.x-1.28"
+                "reference": "8.x-1.30"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.28.zip",
-                "reference": "8.x-1.28",
-                "shasum": "bbfea8791cee7d88be705e0cbf28bd6a22a54c60"
+                "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.30.zip",
+                "reference": "8.x-1.30",
+                "shasum": "1d7983e8e07feee4f13e4b05c9a10db15ae2097e"
             },
             "require": {
                 "drupal/core": "~8.0"
@@ -5208,8 +5212,8 @@
                     "dev-1.x": "1.x-dev"
                 },
                 "drupal": {
-                    "version": "8.x-1.28",
-                    "datestamp": "1533849180",
+                    "version": "8.x-1.30",
+                    "datestamp": "1576274288",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -5223,8 +5227,16 @@
             "authors": [
                 {
                     "name": "Jeff Geerling",
-                    "homepage": "https://www.drupal.org/user/389011",
+                    "homepage": "https://www.drupal.org/user/213194",
                     "email": "geerlingguy@mac.com"
+                },
+                {
+                    "name": "geerlingguy",
+                    "homepage": "https://www.drupal.org/user/389011"
+                },
+                {
+                    "name": "vijaycs85",
+                    "homepage": "https://www.drupal.org/user/93488"
                 }
             ],
             "description": "Mitigates spam form submissions using the honeypot method.",
@@ -5238,7 +5250,7 @@
                 "spam"
             ],
             "support": {
-                "source": "http://cgit.drupalcode.org/honeypot"
+                "source": "https://git.drupalcode.org/project/honeypot"
             }
         },
         {
@@ -5711,17 +5723,17 @@
         },
         {
             "name": "drupal/media_entity_twitter",
-            "version": "2.0.0-alpha2",
+            "version": "2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/media_entity_twitter.git",
-                "reference": "8.x-2.0-alpha2"
+                "reference": "8.x-2.3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.0-alpha2.zip",
-                "reference": "8.x-2.0-alpha2",
-                "shasum": "21925e1e1b02bbbcd6d8e9730bc587669edc0e5c"
+                "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.3.zip",
+                "reference": "8.x-2.3",
+                "shasum": "de0f25deaa97e09c6851c09ee01c1e2f3f505f48"
             },
             "require": {
                 "drupal/core": "^8.4",
@@ -5733,11 +5745,11 @@
                     "dev-2.x": "2.x-dev"
                 },
                 "drupal": {
-                    "version": "8.x-2.0-alpha2",
-                    "datestamp": "1507907344",
+                    "version": "8.x-2.3",
+                    "datestamp": "1579597383",
                     "security-coverage": {
-                        "status": "not-covered",
-                        "message": "Alpha releases are not covered by Drupal security advisories."
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
                     }
                 }
             },
@@ -5758,15 +5770,19 @@
                     "name": "chr.fritsch",
                     "homepage": "https://www.drupal.org/user/2103716"
                 },
+                {
+                    "name": "phenaproxima",
+                    "homepage": "https://www.drupal.org/user/205645"
+                },
                 {
                     "name": "slashrsm",
                     "homepage": "https://www.drupal.org/user/744628"
                 }
             ],
-            "description": "Media entity Twitter provider.",
+            "description": "Media Entity Twitter provider.",
             "homepage": "https://www.drupal.org/project/media_entity_twitter",
             "support": {
-                "source": "http://cgit.drupalcode.org/media_entity_twitter"
+                "source": "https://git.drupalcode.org/project/media_entity_twitter"
             }
         },
         {
@@ -6064,8 +6080,7 @@
             "homepage": "https://www.drupal.org/project/migrate_devel",
             "support": {
                 "source": "http://cgit.drupalcode.org/migrate_devel"
-            },
-            "time": "2017-06-25T23:46:13+00:00"
+            }
         },
         {
             "name": "drupal/migrate_plus",
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 1dcf950ce4129566464e6f27ae1a5efed7f4661c..c55698ce71041d090964b9b4b503c0c2c98c7506 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -2178,21 +2178,21 @@
     },
     {
         "name": "drupal/admin_toolbar",
-        "version": "2.0.0",
-        "version_normalized": "2.0.0.0",
+        "version": "2.2.0",
+        "version_normalized": "2.2.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/admin_toolbar.git",
-            "reference": "8.x-2.0"
+            "reference": "8.x-2.2"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.0.zip",
-            "reference": "8.x-2.0",
-            "shasum": "568de63dbaa8046a82d327dbd0b892ab79fb87aa"
+            "url": "https://ftp.drupal.org/files/projects/admin_toolbar-8.x-2.2.zip",
+            "reference": "8.x-2.2",
+            "shasum": "41ea0e3321e6d1e190c486be49a99e60446d8dd7"
         },
         "require": {
-            "drupal/core": "*"
+            "drupal/core": "^8.8.0 || ^9.0"
         },
         "type": "drupal-module",
         "extra": {
@@ -2200,8 +2200,8 @@
                 "dev-2.x": "2.x-dev"
             },
             "drupal": {
-                "version": "8.x-2.0",
-                "datestamp": "1573751237",
+                "version": "8.x-2.2",
+                "datestamp": "1585017179",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -4748,24 +4748,24 @@
     },
     {
         "name": "drupal/entity_reference_revisions",
-        "version": "1.3.0",
-        "version_normalized": "1.3.0.0",
+        "version": "1.8.0",
+        "version_normalized": "1.8.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/entity_reference_revisions.git",
-            "reference": "8.x-1.3"
+            "reference": "8.x-1.8"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.3.zip",
-            "reference": "8.x-1.3",
-            "shasum": "78aebb58efbbfcbb2faa40a1afc0830312b32631"
+            "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.8.zip",
+            "reference": "8.x-1.8",
+            "shasum": "c1279e6c683edc2dbccedba8de1505340c8a62b6"
         },
         "require": {
-            "drupal/core": "~8.0"
+            "drupal/core": "^8.7.7 || ^9"
         },
         "require-dev": {
-            "drupal/diff": "*"
+            "drupal/diff": "1.x-dev"
         },
         "type": "drupal-module",
         "extra": {
@@ -4773,8 +4773,8 @@
                 "dev-1.x": "1.x-dev"
             },
             "drupal": {
-                "version": "8.x-1.3",
-                "datestamp": "1515143885",
+                "version": "8.x-1.8",
+                "datestamp": "1583961846",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -4784,9 +4784,13 @@
         "installation-source": "dist",
         "notification-url": "https://packages.drupal.org/8/downloads",
         "license": [
-            "GPL-2.0-or-later"
+            "GPL-2.0"
         ],
         "authors": [
+            {
+                "name": "Berdir",
+                "homepage": "https://www.drupal.org/user/214652"
+            },
             {
                 "name": "Frans",
                 "homepage": "https://www.drupal.org/user/514222"
@@ -4800,10 +4804,10 @@
                 "homepage": "https://www.drupal.org/user/227761"
             }
         ],
-        "description": "Adds a Entity Reference field type with revision support.",
+        "description": "Entity Reference Revisions",
         "homepage": "https://www.drupal.org/project/entity_reference_revisions",
         "support": {
-            "source": "http://cgit.drupalcode.org/entity_reference_revisions"
+            "source": "https://git.drupalcode.org/project/entity_reference_revisions"
         }
     },
     {
@@ -5343,18 +5347,18 @@
     },
     {
         "name": "drupal/honeypot",
-        "version": "1.28.0",
-        "version_normalized": "1.28.0.0",
+        "version": "1.30.0",
+        "version_normalized": "1.30.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/honeypot.git",
-            "reference": "8.x-1.28"
+            "reference": "8.x-1.30"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.28.zip",
-            "reference": "8.x-1.28",
-            "shasum": "bbfea8791cee7d88be705e0cbf28bd6a22a54c60"
+            "url": "https://ftp.drupal.org/files/projects/honeypot-8.x-1.30.zip",
+            "reference": "8.x-1.30",
+            "shasum": "1d7983e8e07feee4f13e4b05c9a10db15ae2097e"
         },
         "require": {
             "drupal/core": "~8.0"
@@ -5365,8 +5369,8 @@
                 "dev-1.x": "1.x-dev"
             },
             "drupal": {
-                "version": "8.x-1.28",
-                "datestamp": "1533849180",
+                "version": "8.x-1.30",
+                "datestamp": "1576274288",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -5381,8 +5385,16 @@
         "authors": [
             {
                 "name": "Jeff Geerling",
-                "homepage": "https://www.drupal.org/user/389011",
+                "homepage": "https://www.drupal.org/user/213194",
                 "email": "geerlingguy@mac.com"
+            },
+            {
+                "name": "geerlingguy",
+                "homepage": "https://www.drupal.org/user/389011"
+            },
+            {
+                "name": "vijaycs85",
+                "homepage": "https://www.drupal.org/user/93488"
             }
         ],
         "description": "Mitigates spam form submissions using the honeypot method.",
@@ -5396,7 +5408,7 @@
             "spam"
         ],
         "support": {
-            "source": "http://cgit.drupalcode.org/honeypot"
+            "source": "https://git.drupalcode.org/project/honeypot"
         }
     },
     {
@@ -5885,18 +5897,18 @@
     },
     {
         "name": "drupal/media_entity_twitter",
-        "version": "2.0.0-alpha2",
-        "version_normalized": "2.0.0.0-alpha2",
+        "version": "2.3.0",
+        "version_normalized": "2.3.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/media_entity_twitter.git",
-            "reference": "8.x-2.0-alpha2"
+            "reference": "8.x-2.3"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.0-alpha2.zip",
-            "reference": "8.x-2.0-alpha2",
-            "shasum": "21925e1e1b02bbbcd6d8e9730bc587669edc0e5c"
+            "url": "https://ftp.drupal.org/files/projects/media_entity_twitter-8.x-2.3.zip",
+            "reference": "8.x-2.3",
+            "shasum": "de0f25deaa97e09c6851c09ee01c1e2f3f505f48"
         },
         "require": {
             "drupal/core": "^8.4",
@@ -5908,11 +5920,11 @@
                 "dev-2.x": "2.x-dev"
             },
             "drupal": {
-                "version": "8.x-2.0-alpha2",
-                "datestamp": "1507907344",
+                "version": "8.x-2.3",
+                "datestamp": "1579597383",
                 "security-coverage": {
-                    "status": "not-covered",
-                    "message": "Alpha releases are not covered by Drupal security advisories."
+                    "status": "covered",
+                    "message": "Covered by Drupal's security advisory policy"
                 }
             }
         },
@@ -5934,15 +5946,19 @@
                 "name": "chr.fritsch",
                 "homepage": "https://www.drupal.org/user/2103716"
             },
+            {
+                "name": "phenaproxima",
+                "homepage": "https://www.drupal.org/user/205645"
+            },
             {
                 "name": "slashrsm",
                 "homepage": "https://www.drupal.org/user/744628"
             }
         ],
-        "description": "Media entity Twitter provider.",
+        "description": "Media Entity Twitter provider.",
         "homepage": "https://www.drupal.org/project/media_entity_twitter",
         "support": {
-            "source": "http://cgit.drupalcode.org/media_entity_twitter"
+            "source": "https://git.drupalcode.org/project/media_entity_twitter"
         }
     },
     {
diff --git a/web/modules/admin_toolbar/admin_toolbar.info.yml b/web/modules/admin_toolbar/admin_toolbar.info.yml
index b55a509cffc796765208f3105fcca7b074c03c77..382e37ffb632cbf53b8a88d3ddf18e953a7b7a4b 100644
--- a/web/modules/admin_toolbar/admin_toolbar.info.yml
+++ b/web/modules/admin_toolbar/admin_toolbar.info.yml
@@ -3,12 +3,12 @@ description: Provides an improved drop-down menu interface to the site Toolbar.
 package: Administration
 
 type: module
-core: 8.x
+core_version_requirement: ^8.8.0 || ^9.0
 
 dependencies:
   - drupal:toolbar
 
-# Information added by Drupal.org packaging script on 2019-10-29
-version: '8.x-2.0'
+# Information added by Drupal.org packaging script on 2020-03-24
+version: '8.x-2.2'
 project: 'admin_toolbar'
-datestamp: 1572370993
+datestamp: 1585017182
diff --git a/web/modules/admin_toolbar/admin_toolbar.libraries.yml b/web/modules/admin_toolbar/admin_toolbar.libraries.yml
index 34306becdb84df10b5c3eb40e0aac95051aaa295..85b322a4e6f1bf851f781529e6140a7e66acd86b 100755
--- a/web/modules/admin_toolbar/admin_toolbar.libraries.yml
+++ b/web/modules/admin_toolbar/admin_toolbar.libraries.yml
@@ -8,14 +8,3 @@ toolbar.tree:
   dependencies:
     - core/jquery
     - core/drupal
-search:
-  css:
-    theme:
-      css/admin.toolbar_search.css: {}
-  js:
-    js/admin_toolbar_search.js: {}
-  dependencies:
-    - core/jquery
-    - core/drupal
-    - core/jquery.once
-    - core/jquery.ui.autocomplete
diff --git a/web/modules/admin_toolbar/admin_toolbar.module b/web/modules/admin_toolbar/admin_toolbar.module
index 3c5e81bda093621abd53be5d6507e8c576e97a49..c02c3f4868df2827ffd830eb170de560b15bab62 100755
--- a/web/modules/admin_toolbar/admin_toolbar.module
+++ b/web/modules/admin_toolbar/admin_toolbar.module
@@ -5,57 +5,17 @@
  * This is the module to create a drop-down menu for the core toolbar.
  */
 
-use Drupal\Core\Menu\MenuTreeParameters;
+use Drupal\admin_toolbar\Render\Element\AdminToolbar;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 use Drupal\Component\Utility\Html;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Implements hook_toolbar_alter().
  */
 function admin_toolbar_toolbar_alter(&$items) {
-  $items['administration']['tray']['toolbar_administration']['#pre_render'] = ['admin_toolbar_prerender_toolbar_administration_tray'];
+  $items['administration']['tray']['toolbar_administration']['#pre_render'] = [[AdminToolbar::class, 'preRenderTray']];
   $items['administration']['#attached']['library'][] = 'admin_toolbar/toolbar.tree';
-  $admin_toolbar_tools = \Drupal::service('module_handler')
-    ->moduleExists('admin_toolbar_tools');
-  $items['administration_search'] = [
-    "#type" => "toolbar_item",
-    'tab' => [
-      '#type' => 'link',
-      '#title' => new TranslatableMarkup('Search'),
-      '#url' => URL::fromRoute('system.admin'),
-      '#attributes' => [
-        'class' => [
-          'toolbar-icon',
-        ],
-      ],
-    ],
-    'tray' => [
-      'search' => [
-        '#title' => 'Search',
-        '#type' => 'textfield',
-        '#size' => 60,
-        '#attributes' => [
-          'id' => 'admin-toolbar-search-input',
-          'aria-labelledby' => 'toolbar-item-administration-search',
-        ],
-      ],
-    ],
-    '#attached' => [
-      'library' => [
-        'admin_toolbar/search',
-      ],
-      'drupalSettings' => [
-        'adminToolbarSearch' => [
-          'loadExtraLinks' => $admin_toolbar_tools,
-        ],
-      ],
-    ],
-    '#wrapper_attributes' => [
-      "id" => "admin-toolbar-search-tab",
-    ],
-  ];
 }
 
 /**
@@ -77,35 +37,6 @@ function admin_toolbar_help($route_name, RouteMatchInterface $route_match) {
   }
 }
 
-/**
- * Renders the toolbar's administration tray.
- *
- * This is a clone of core's toolbar_prerender_toolbar_administration_tray()
- * function, which uses setMaxDepth(4) instead of setTopLevelOnly().
- *
- * @param array $element
- *   A renderable array.
- *
- * @return array
- *   The updated renderable array.
- *
- * @see toolbar_prerender_toolbar_administration_tray()
- */
-function admin_toolbar_prerender_toolbar_administration_tray(array $element) {
-  $menu_tree = \Drupal::service('toolbar.menu_tree');
-  $parameters = new MenuTreeParameters();
-  $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(4)->onlyEnabledLinks();
-  $tree = $menu_tree->load(NULL, $parameters);
-  $manipulators = [
-    ['callable' => 'menu.default_tree_manipulators:checkAccess'],
-    ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
-    ['callable' => 'toolbar_tools_menu_navigation_links'],
-  ];
-  $tree = $menu_tree->transform($tree, $manipulators);
-  $element['administration_menu'] = $menu_tree->build($tree);
-  return $element;
-}
-
 /**
  * Adds toolbar-specific attributes to the menu link tree.
  *
diff --git a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml
index 9ac6cdb5a387e84a74bbd69b663f63277aa8c0ae..1b8b648077189fd2a7441a268e4a2abec439768f 100644
--- a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml
+++ b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/admin_toolbar_links_access_filter.info.yml
@@ -3,12 +3,12 @@ description: Provides a workaround for the common problem that users with 'Use t
 package: Administration
 
 type: module
-core: 8.x
+core_version_requirement: ^8.8.0 || ^9.0
 
 dependencies:
   - admin_toolbar:admin_toolbar
 
-# Information added by Drupal.org packaging script on 2019-10-29
-version: '8.x-2.0'
+# Information added by Drupal.org packaging script on 2020-03-24
+version: '8.x-2.2'
 project: 'admin_toolbar'
-datestamp: 1572370993
+datestamp: 1585017182
diff --git a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json
index d75f3b0c68ce40945ebd60764c3b2bd98e30b89c..b7a007d7be8a5f115b3167e675baad91560418a8 100755
--- a/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json
+++ b/web/modules/admin_toolbar/admin_toolbar_links_access_filter/composer.json
@@ -26,13 +26,14 @@
       "name": "Mohamed Anis Taktak (matio89)",
       "homepage": "https://www.drupal.org/u/matio89",
       "role": "Maintainer"
-    }    
+    }
   ],
   "support": {
     "issues": "https://www.drupal.org/project/issues/admin_toolbar",
     "source": "http://cgit.drupalcode.org/admin_toolbar"
   },
   "require": {
-    "drupal/admin_toolbar": "^1"
+    "drupal/admin_toolbar": "^2",
+    "drupal/core": "^8.8.0 || ^9.0"
   }
 }
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3dcad3ac3cfa7958994009a3a2f72a533a0eb6e8
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.info.yml
@@ -0,0 +1,14 @@
+name: Admin Toolbar Search
+description: Provides search of admin toolbar items.
+package: Administration
+
+type: module
+core_version_requirement: ^8.8.0 || ^9.0
+
+dependencies:
+  - admin_toolbar:admin_toolbar
+
+# Information added by Drupal.org packaging script on 2020-03-24
+version: '8.x-2.2'
+project: 'admin_toolbar'
+datestamp: 1585017182
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.libraries.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4f3bedac46babd32003f1b15844bf0ad29640869
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.libraries.yml
@@ -0,0 +1,11 @@
+search:
+  css:
+    theme:
+      css/admin.toolbar_search.css: {}
+  js:
+    js/admin_toolbar_search.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/jquery.once
+    - core/jquery.ui.autocomplete
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module
new file mode 100755
index 0000000000000000000000000000000000000000..acad2f23cf0384bf4a413ed76a3c1268c182f31a
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.module
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Functionality for search of Admin Toolbar.
+ */
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function admin_toolbar_search_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help.
+    case 'help.page.admin_toolbar_search':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Admin Toolbar Search module add a search option to the toolbar for site administrative tasks.') . '</p>';
+
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_toolbar_alter().
+ */
+function admin_toolbar_search_toolbar_alter(&$items) {
+  $access = \Drupal::currentUser()->hasPermission('use admin toolbar search');
+  $admin_toolbar_tools_enabled = \Drupal::service('module_handler')
+    ->moduleExists('admin_toolbar_tools');
+
+  $items['administration_search'] = [
+    "#type" => "toolbar_item",
+    '#access' => $access,
+    'tab' => [
+      '#type' => 'link',
+      '#title' => new TranslatableMarkup('Search'),
+      '#url' => URL::fromRoute('system.admin'),
+      '#attributes' => [
+        'class' => [
+          'toolbar-icon',
+        ],
+      ],
+    ],
+    'tray' => [
+      'search' => [
+        '#title' => 'Search',
+        '#type' => 'textfield',
+        '#size' => 60,
+        '#attributes' => [
+          'id' => 'admin-toolbar-search-input',
+          'aria-labelledby' => 'toolbar-item-administration-search',
+        ],
+      ],
+    ],
+    '#attached' => [
+      'library' => [
+        'admin_toolbar_search/search',
+      ],
+      'drupalSettings' => [
+        'adminToolbarSearch' => [
+          'loadExtraLinks' => $admin_toolbar_tools_enabled,
+        ],
+      ],
+    ],
+    '#wrapper_attributes' => [
+      'id' => 'admin-toolbar-search-tab',
+    ],
+    '#cache' => [
+      'contexts' => [
+        'user.permissions',
+      ],
+    ],
+  ];
+
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.permissions.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b0d7ec73c5ad73a0e6a864c75e01dab020f7abba
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.permissions.yml
@@ -0,0 +1,2 @@
+use admin toolbar search:
+  title: 'Use Admin Toolbar search'
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.routing.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.routing.yml
new file mode 100755
index 0000000000000000000000000000000000000000..14adc551599cc40ab387c0ee68b7efadd407f115
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.routing.yml
@@ -0,0 +1,6 @@
+admin_toolbar.search:
+  path: '/admin/admin-toolbar-search'
+  defaults:
+    _controller: '\Drupal\admin_toolbar_search\Controller\AdminToolbarSearchController::search'
+  requirements:
+    _permission: 'use admin toolbar search'
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.services.yml b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..977e6ce2c3aba9cdccca8f7065c799847a953bf5
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/admin_toolbar_search.services.yml
@@ -0,0 +1,9 @@
+services:
+  admin_toolbar_search.search_links:
+    class: Drupal\admin_toolbar_search\SearchLinks
+    arguments:
+     - '@entity_type.manager'
+     - '@module_handler'
+     - '@router.route_provider'
+     - '@cache_contexts_manager'
+     - '@cache.toolbar'
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/composer.json b/web/modules/admin_toolbar/admin_toolbar_search/composer.json
new file mode 100755
index 0000000000000000000000000000000000000000..919f43f82baf791b5aaa52662deea3bc382f2447
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/composer.json
@@ -0,0 +1,39 @@
+{
+  "name": "drupal/admin_toolbar_search",
+  "description": "Provides search of admin toolbar items.",
+  "type": "drupal-module",
+  "keywords": ["Drupal", "Toolbar", "Search"],
+  "homepage": "http://drupal.org/project/admin_toolbar",
+  "license": "GPL-2.0+",
+  "authors": [
+    {
+      "name": "Wilfrid Roze (eme)",
+      "homepage": "https://www.drupal.org/u/eme",
+      "role": "Maintainer"
+    },
+    {
+      "name": "Romain Jarraud (romainj)",
+      "homepage": "https://www.drupal.org/u/romainj",
+      "role": "Maintainer"
+    },
+    {
+      "name": "Adrian Cid Almaguer (adriancid)",
+      "email": "adriancid@gmail.com",
+      "homepage": "https://www.drupal.org/u/adriancid",
+      "role": "Maintainer"
+    },
+    {
+      "name": "Mohamed Anis Taktak (matio89)",
+      "homepage": "https://www.drupal.org/u/matio89",
+      "role": "Maintainer"
+    }
+  ],
+  "support": {
+    "issues": "https://www.drupal.org/project/issues/admin_toolbar",
+    "source": "http://cgit.drupalcode.org/admin_toolbar"
+  },
+  "require": {
+    "drupal/admin_toolbar": "^2",
+    "drupal/core": "^8.8.0 || ^9.0"
+  }
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/css/admin.toolbar_search.css b/web/modules/admin_toolbar/admin_toolbar_search/css/admin.toolbar_search.css
new file mode 100755
index 0000000000000000000000000000000000000000..6215d9b625ea6d9ce16cee0caee2155734314d9d
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/css/admin.toolbar_search.css
@@ -0,0 +1,33 @@
+#toolbar-item-administration-search-tray {
+  padding-left: 1em;
+}
+
+#admin-toolbar-search-tab .toolbar-item:before {
+  background-image: url('../../misc/icons/bebebe/loupe.svg');
+}
+
+#admin-toolbar-search-tab .toolbar-item:active:before,
+#admin-toolbar-search-tab .toolbar-item.is-active:before {
+  background-image: url('../../misc/icons/ffffff/loupe.svg');
+}
+
+#toolbar-item-administration-search-tray label {
+  display: inline-block;
+  color: #000000;
+  margin-right: .5em;
+  font-weight: bold;
+}
+
+#toolbar-item-administration-search-tray div.form-item {
+  margin: 0.75em 0;
+}
+
+#toolbar-item-administration-search-tray input {
+  display: inline-block;
+  padding: 0.3em 0.4em 0.3em 0.5em;
+  font-size: 1em;
+}
+
+.ui-autocomplete .ui-menu-item span.admin-toolbar-search-url {
+  color: rgba(0, 0, 0, 0.50);
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js b/web/modules/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js
new file mode 100755
index 0000000000000000000000000000000000000000..701d76fb6c2caf698eb1f4c3310e904d9368e5e9
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/js/admin_toolbar_search.js
@@ -0,0 +1,188 @@
+/**
+ * @file
+ * Behaviors for the search widget in the admin toolbar.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  Drupal.behaviors.adminToolbarSearch = {
+
+    // If extra links have been fetched.
+    extraFetched: false,
+
+    attach: function (context) {
+      if (context != document) {
+        return;
+      }
+
+      var getUrl = window.location;
+      var baseUrl = getUrl.protocol + "//" + getUrl.host + "/";
+      var $self = this;
+      this.links = [];
+      $('.toolbar-tray a[data-drupal-link-system-path]').each(function () {
+        if (this.href != baseUrl) {
+          var label = $self.getItemLabel(this);
+          $self.links.push({
+            'value': $(this).attr('href'),
+            'label': label + ' ' + $(this).attr('href'),
+            'labelRaw': label
+          });
+        }
+      });
+
+      $("#admin-toolbar-search-input").autocomplete({
+        minLength: 2,
+        source: function (request, response) {
+          var data = $self.handleAutocomplete(request.term);
+          if (!$self.extraFetched && drupalSettings.adminToolbarSearch.loadExtraLinks) {
+            $.getJSON( "/admin/admin-toolbar-search", function( data ) {
+              $(data).each(function() {
+                var item = this;
+                item.label = this.labelRaw + ' ' + this.value;
+                $self.links.push(item);
+              });
+
+              $self.extraFetched = true;
+
+              var results = $self.handleAutocomplete(request.term);
+              response(results);
+            });
+          }
+          else {
+            response(data);
+          }
+        },
+        open: function () {
+          var zIndex = $('#toolbar-item-administration-search-tray')
+            .css("z-index") + 1;
+          $(this).autocomplete('widget').css('z-index', zIndex);
+
+          return false;
+        },
+        select: function (event, ui) {
+          if (ui.item.value) {
+            location.href = ui.item.value;
+            return false;
+          }
+        }
+      }).data("ui-autocomplete")._renderItem = (function (ul, item) {
+        return $("<li>")
+          .append('<div>' + item.labelRaw + ' <span class="admin-toolbar-search-url">' + item.value + '</span></div>')
+          .appendTo(ul);
+      });
+
+      // Focus on search field when tab is clicked, or enter is pressed.
+      $(context).find('#toolbar-item-administration-search')
+        .once('admin_toolbar_search')
+        .each(function () {
+          if (Drupal.behaviors.adminToolbarSearch.isSearchVisible()) {
+            $('#admin-toolbar-search-input').focus();
+          }
+          $(this).on('click', function () {
+            $self.focusOnSearchElement();
+          });
+        });
+
+      // Initialize hotkey / keyboard shortcut.
+      this.initHotkey();
+    },
+    focusOnSearchElement: function () {
+      var waitforVisible = function () {
+        if ($('#toolbar-item-administration-search-tray:visible').length) {
+          $('#admin-toolbar-search-input').focus();
+        }
+        else {
+          setTimeout(function () {
+            waitforVisible();
+          }, 1);
+        }
+      };
+      waitforVisible();
+    },
+    getItemLabel: function (item) {
+      var breadcrumbs = [];
+      $(item).parents().each(function () {
+        if ($(this).hasClass('menu-item')) {
+          var $link = $(this).find('a:first');
+          if ($link.length && !$link.hasClass('admin-toolbar-search-ignore')) {
+            breadcrumbs.unshift($link.text());
+          }
+        }
+      });
+      return breadcrumbs.join(' > ');
+    },
+    handleAutocomplete: function (term) {
+      var $self = this;
+      var keywords = term.split(" "); // Split search terms into list.
+
+      var suggestions = [];
+      $self.links.forEach(function (element) {
+        var label = element.label.toLowerCase();
+
+        // Add exact matches.
+        if (label.indexOf(term.toLowerCase()) >= 0) {
+          suggestions.push(element);
+        }
+        else {
+          // Add suggestions where it matches all search terms.
+          var matchCount = 0;
+          keywords.forEach(function (keyword) {
+            if (label.indexOf(keyword.toLowerCase()) >= 0) {
+              matchCount++;
+            }
+          });
+          if (matchCount == keywords.length) {
+            suggestions.push(element);
+          }
+        }
+      });
+      return suggestions;
+    },
+    /**
+     * Whether the search is visible or not.
+     *
+     * @returns {boolean}
+     *   True if visible, false otherwise.
+     */
+    isSearchVisible: function () {
+      return $('#toolbar-item-administration-search-tray').is(':visible');
+    },
+    /**
+     * Toggles the toolbar search tray.
+     */
+    toggleSearch: function () {
+      $('#toolbar-item-administration-search').trigger('click');
+    },
+    /**
+     * Binds a keyboard shortcut to toggle the search.
+     */
+    initHotkey: function () {
+      $(document)
+        .once('admin_toolbar_search')
+        .keydown(function (event) {
+          // Show the form with alt + S.
+          if (!Drupal.behaviors.adminToolbarSearch.isSearchVisible()) {
+            // 83 = s.
+            if (event.altKey === true && event.keyCode === 83) {
+              Drupal.behaviors.adminToolbarSearch.toggleSearch();
+              event.preventDefault();
+            }
+          }
+          // Hide the search with alt + S or ESC.
+          else {
+            // 83 = s.
+            if (
+              (event.altKey === true && event.keyCode === 83) ||
+              event.key === 'Escape'
+            ) {
+              Drupal.behaviors.adminToolbarSearch.toggleSearch();
+              event.preventDefault();
+            }
+          }
+        });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/src/Controller/AdminToolbarSearchController.php b/web/modules/admin_toolbar/admin_toolbar_search/src/Controller/AdminToolbarSearchController.php
new file mode 100755
index 0000000000000000000000000000000000000000..d3ae3325b72811057ed7f351b51208ba3e689e2c
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/src/Controller/AdminToolbarSearchController.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\admin_toolbar_search\Controller;
+
+use Drupal\admin_toolbar_search\SearchLinks;
+use Drupal\Core\Controller\ControllerBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+/**
+ * Class AdminToolbarSearchController.
+ *
+ * @package Drupal\admin_toolbar_tools\Controller
+ */
+class AdminToolbarSearchController extends ControllerBase {
+
+  /**
+   * The search links service.
+   *
+   * @var \Drupal\admin_toolbar_search\SearchLinks
+   */
+  protected $links;
+
+  /**
+   * Constructs an AdminToolbarSearchController object.
+   *
+   * @param \Drupal\admin_toolbar_search\SearchLinks $links
+   *   The search links service.
+   */
+  public function __construct(SearchLinks $links) {
+    $this->links = $links;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('admin_toolbar_search.search_links')
+    );
+  }
+
+  /**
+   * Return additional search links.
+   */
+  public function search() {
+    return new JsonResponse($this->links->getLinks());
+  }
+
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/SearchLinks.php b/web/modules/admin_toolbar/admin_toolbar_search/src/SearchLinks.php
similarity index 92%
rename from web/modules/admin_toolbar/admin_toolbar_tools/src/SearchLinks.php
rename to web/modules/admin_toolbar/admin_toolbar_search/src/SearchLinks.php
index 703edbee5311bebbf5389d3a7cf27874e9c13401..e36dba37a9617b3de26c7728fa4dc198f9dc3b9c 100644
--- a/web/modules/admin_toolbar/admin_toolbar_tools/src/SearchLinks.php
+++ b/web/modules/admin_toolbar/admin_toolbar_search/src/SearchLinks.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\admin_toolbar_tools;
+namespace Drupal\admin_toolbar_search;
 
 use Drupal\admin_toolbar_tools\Plugin\Derivative\ExtraLinks;
 use Drupal\Core\Cache\Cache;
@@ -57,7 +57,18 @@ class SearchLinks {
   protected $toolbarCache;
 
   /**
-   * {@inheritdoc}
+   * Constructs a SearchLinks object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider.
+   * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_context_manager
+   *   The cache contexts manager.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $toolbar_cache
+   *   Cache backend instance to use.
    */
   public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, CacheContextsManager $cache_context_manager, CacheBackendInterface $toolbar_cache) {
     $this->entityTypeManager = $entity_type_manager;
@@ -68,10 +79,10 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Mod
   }
 
   /**
-   * Get extra links for admin toolbar search feature.
+   * Gets extra links for admin toolbar search feature.
    *
    * @return array
-   *   An array of link data.
+   *   An array of link data for the JSON used for search.
    *
    * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
@@ -239,7 +250,7 @@ public function getLinks() {
   }
 
   /**
-   * Get a list of content entities.
+   * Gets a list of content entities.
    *
    * @return array
    *   An array of metadata about content entities.
@@ -258,19 +269,6 @@ protected function getBundleableEntitiesList() {
     return $content_entities;
   }
 
-  /**
-   * Get an array of entity types that should trigger a menu rebuild.
-   *
-   * @return array
-   *   An array of entity machine names.
-   */
-  public function getRebuildEntityTypes() {
-    $types = ['menu'];
-    $content_entities = $this->getBundleableEntitiesList();
-    $types = array_merge($types, array_column($content_entities, 'content_entity_bundle'));
-    return $types;
-  }
-
   /**
    * Determine if a route exists by name.
    *
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php
new file mode 100755
index 0000000000000000000000000000000000000000..f12b8f8543c77c0fa73aba069a55195646199c96
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\Tests\admin_toolbar_search\FunctionalJavascript;
+
+/**
+ * Test the functionality of admin toolbar search.
+ *
+ * @group admin_toolbar
+ * @group admin_toolbar_search
+ */
+class AdminToolbarSearchTest extends AdminToolbarSearchTestBase {
+
+  /**
+   * Tests search functionality without admin_toolbar_tools enabled.
+   */
+  public function testToolbarSearch() {
+    $search_tab = '#toolbar-item-administration-search';
+    $search_tray = '#toolbar-item-administration-search-tray';
+
+    $this->drupalLogin($this->userWithAccess);
+    $assert_session = $this->assertSession();
+    $assert_session->responseContains('admin.toolbar_search.css');
+    $assert_session->responseContains('admin_toolbar_search.js');
+    $assert_session->waitForElementVisible('css', $search_tab)->click();
+    $assert_session->waitForElementVisible('css', $search_tray);
+
+    $this->assertSuggestionContains('perfor', 'admin/config/development/performance');
+    $this->assertSuggestionContains('develop', 'admin/config/development/maintenance');
+    $this->assertSuggestionContains('types', 'admin/structure/types');
+  }
+
+  /**
+   * Tests a user without the search permission can't use search.
+   */
+  public function testNoAccess() {
+    $search_tab = '#toolbar-item-administration-search';
+    $search_tray = '#toolbar-item-administration-search-tray';
+
+    $this->drupalLogin($this->noAccessUser);
+    $assert_session = $this->assertSession();
+    $assert_session->responseNotContains('admin.toolbar_search.css');
+    $assert_session->responseNotContains('admin_toolbar_search.js');
+    $assert_session->elementNotExists('css', $search_tab);
+    $assert_session->elementNotExists('css', $search_tray);
+  }
+
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTestBase.php b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTestBase.php
new file mode 100755
index 0000000000000000000000000000000000000000..6a16c5882e0f863a4a4ca078a6f163fc3e911859
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarSearchTestBase.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\Tests\admin_toolbar_search\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\system\Entity\Menu;
+
+/**
+ * Base class for testing the functionality of admin toolbar search.
+ *
+ * @group admin_toolbar
+ * @group admin_toolbar_search
+ */
+abstract class AdminToolbarSearchTestBase extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'admin_toolbar_search',
+    'node',
+    'media',
+    'field_ui',
+    'menu_ui',
+    'block',
+  ];
+
+  /**
+   * A user with the 'Use Admin Toolbar search' permission.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $userWithAccess;
+
+  /**
+   * A test user without the 'Use Admin Toolbar search' permission..
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $noAccessUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $baby_names = [
+      'ada' => 'Ada',
+      'amara' => 'Amara',
+      'amelia' => 'Amelia',
+      'arabella' => 'Arabella',
+      'asher' => 'Asher',
+      'astrid' => 'Astrid',
+      'atticus' => 'Atticus',
+      'aurora' => 'Aurora',
+      'ava' => 'Ava',
+      'cora' => 'Cora',
+      'eleanor' => 'Eleanor',
+      'eloise' => 'Eloise',
+      'felix' => 'Felix',
+      'freya' => 'Freya',
+      'genevieve' => 'Genevieve',
+      'isla' => 'Isla',
+      'jasper' => 'Jasper',
+      'luna' => 'Luna',
+      'maeve' => 'Maeve',
+      'milo' => 'Milo',
+      'nora' => 'Nora',
+      'olivia' => 'Olivia',
+      'ophelia' => 'Ophelia',
+      'posie' => 'Posie',
+      'rose' => 'Rose',
+      'silas' => 'Silas',
+      'soren' => 'Soren',
+    ];
+
+    foreach ($baby_names as $id => $label) {
+      $menu = Menu::create([
+        'id' => $id,
+        'label' => $label,
+      ]);
+      $menu->save();
+    }
+
+    $this->drupalPlaceBlock('local_tasks_block');
+
+    $permissions = [
+      'access toolbar',
+      'administer menu',
+      'access administration pages',
+      'administer site configuration',
+      'administer content types',
+    ];
+    $this->noAccessUser = $this->drupalCreateUser($permissions);
+    $permissions[] = 'use admin toolbar search';
+    $this->userWithAccess = $this->drupalCreateUser($permissions);
+  }
+
+  /**
+   * Assert that the search suggestions contain a given string with given input.
+   *
+   * @param string $search
+   *   The string to search for.
+   * @param string $contains
+   *   Some HTML that is expected to be within the suggestions element.
+   */
+  protected function assertSuggestionContains($search, $contains) {
+    $this->resetSearch();
+    $page = $this->getSession()->getPage();
+    $page->fillField('admin-toolbar-search-input', $search);
+    $this->getSession()->getDriver()->keyDown('//input[@id="admin-toolbar-search-input"]', ' ');
+    $page->waitFor(3, function () use ($page) {
+      return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE);
+    });
+    $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml();
+    $this->assertContains($contains, $suggestions_markup);
+  }
+
+  /**
+   * Assert that the search suggestions does not contain a given string.
+   *
+   * Assert that the search suggestions does not contain a given string with a
+   * given input.
+   *
+   * @param string $search
+   *   The string to search for.
+   * @param string $contains
+   *   Some HTML that is not expected to be within the suggestions element.
+   */
+  protected function assertSuggestionNotContains($search, $contains) {
+    $this->resetSearch();
+    $page = $this->getSession()->getPage();
+    $page->fillField('admin-toolbar-search-input', $search);
+    $this->getSession()->getDriver()->keyDown('//input[@id="admin-toolbar-search-input"]', ' ');
+    $page->waitFor(3, function () use ($page) {
+      return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE);
+    });
+    if ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE) {
+      return;
+    }
+    else {
+      $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml();
+      $this->assertNotContains($contains, $suggestions_markup);
+    }
+  }
+
+  /**
+   * Search for an empty string to clear out the autocomplete suggestions.
+   */
+  protected function resetSearch() {
+    $page = $this->getSession()->getPage();
+    // Empty out the suggestions.
+    $page->fillField('admin-toolbar-search-input', '');
+    $this->getSession()->getDriver()->keyDown('//input[@id="admin-toolbar-search-input"]', ' ');
+    $page->waitFor(3, function () use ($page) {
+      return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE);
+    });
+  }
+
+  /**
+   * Checks that there is a link with the specified url in the admin toolbar.
+   *
+   * @param string $url
+   *   The url to assert exists in the admin menu.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  protected function assertMenuHasHref($url) {
+    $this->assertSession()
+      ->elementExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]');
+  }
+
+  /**
+   * Checks that there is no link with the specified url in the admin toolbar.
+   *
+   * @param string $url
+   *   The url to assert exists in the admin menu.
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   */
+  protected function assertMenuDoesNotHaveHref($url) {
+    $this->assertSession()
+      ->elementNotExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]');
+  }
+
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarToolsSearchTest.php b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarToolsSearchTest.php
new file mode 100755
index 0000000000000000000000000000000000000000..239b6ef332fdf4643ddb53c29ce272bb34cc0345
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_search/tests/src/FunctionalJavascript/AdminToolbarToolsSearchTest.php
@@ -0,0 +1,213 @@
+<?php
+
+namespace Drupal\Tests\admin_toolbar_search\FunctionalJavascript;
+
+use Drupal\media\Entity\MediaType;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+
+/**
+ * Test the functionality of admin toolbar search.
+ *
+ * @group admin_toolbar
+ * @group admin_toolbar_search
+ */
+class AdminToolbarToolsSearchTest extends AdminToolbarSearchTestBase {
+
+  use MediaTypeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'admin_toolbar_tools',
+    'admin_toolbar_search',
+    'node',
+    'media',
+    'field_ui',
+    'menu_ui',
+    'block',
+  ];
+
+  /**
+   * The admin user for tests.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->drupalCreateContentType([
+      'type' => 'article',
+      'name' => 'Article',
+    ]);
+
+    $dog_names = [
+      'archie' => 'Archie',
+      'bailey' => 'Bailey',
+      'bella' => 'Bella',
+      'buddy' => 'Buddy',
+      'charlie' => 'Charlie',
+      'coco' => 'Coco',
+      'daisy' => 'Daisy',
+      'frankie' => 'Frankie',
+      'jack' => 'Jack',
+      'lola' => 'Lola',
+      'lucy' => 'Lucy',
+      'max' => 'Max',
+      'milo' => 'Milo',
+      'molly' => 'Molly',
+      'ollie' => 'Ollie',
+      'oscar' => 'Oscar',
+      'rosie' => 'Rosie',
+      'ruby' => 'Ruby',
+      'teddy' => 'Teddy',
+      'toby' => 'Toby',
+    ];
+
+    foreach ($dog_names as $machine_name => $label) {
+      $this->createMediaType('image', [
+        'id' => $machine_name,
+        'label' => $label,
+      ]);
+    }
+
+    $this->adminUser = $this->drupalCreateUser([
+      'access toolbar',
+      'administer menu',
+      'access administration pages',
+      'administer site configuration',
+      'administer content types',
+      'administer node fields',
+      'access media overview',
+      'administer media',
+      'administer media fields',
+      'administer media form display',
+      'administer media display',
+      'administer media types',
+      'use admin toolbar search',
+    ]);
+  }
+
+  /**
+   * Tests search functionality with admin_toolbar_tools enabled.
+   */
+  public function testToolbarSearch() {
+    $search_tab = '#toolbar-item-administration-search';
+    $search_tray = '#toolbar-item-administration-search-tray';
+
+    $this->drupalLogin($this->adminUser);
+    $assert_session = $this->assertSession();
+    $assert_session->responseContains('admin.toolbar_search.css');
+    $assert_session->responseContains('admin_toolbar_search.js');
+    $assert_session->waitForElementVisible('css', $search_tab)->click();
+    $assert_session->waitForElementVisible('css', $search_tray);
+
+    $this->assertSuggestionContains('basic', 'admin/config/system/site-information');
+
+    // Rebuild menu items.
+    drupal_flush_all_caches();
+
+    // Test that the route admin_toolbar.search returns expected json.
+    $this->drupalGet('/admin/admin-toolbar-search');
+
+    $search_menus = [
+      'cora',
+      'eleanor',
+      'eloise',
+      'felix',
+      'freya',
+      'genevieve',
+      'isla',
+      'jasper',
+      'luna',
+      'maeve',
+      'milo',
+      'nora',
+      'olivia',
+      'ophelia',
+      'posie',
+      'rose',
+      'silas',
+      'soren',
+    ];
+
+    $toolbar_menus = [
+      'ada',
+      'amara',
+      'amelia',
+      'arabella',
+      'asher',
+      'astrid',
+      'atticus',
+      'aurora',
+      'ava',
+    ];
+
+    foreach ($search_menus as $menu_id) {
+      $assert_session->responseContains('\/admin\/structure\/menu\/manage\/' . $menu_id);
+    }
+
+    foreach ($toolbar_menus as $menu_id) {
+      $assert_session->responseNotContains('\/admin\/structure\/menu\/manage\/' . $menu_id);
+    }
+
+    $this->drupalGet('/admin');
+
+    foreach ($search_menus as $menu_id) {
+      $this->assertMenuDoesNotHaveHref('/admin/structure/menu/manage/' . $menu_id);
+    }
+
+    foreach ($toolbar_menus as $menu_id) {
+      $this->assertMenuHasHref('/admin/structure/menu/manage/' . $menu_id);
+    }
+
+    $this->drupalGet('admin/structure/types/manage/article/fields');
+    $assert_session->waitForElementVisible('css', $search_tray);
+
+    $this->assertSuggestionContains('article manage fields', '/admin/structure/types/manage/article/fields');
+
+    $suggestions = $assert_session
+      ->waitForElementVisible('css', 'ul.ui-autocomplete');
+
+    // Assert there is only one suggestion with a link to
+    // /admin/structure/types/manage/article/fields.
+    $count = count($suggestions->findAll('xpath', '//span[contains(text(), "/admin/structure/types/manage/article/fields")]'));
+    $this->assertEquals(1, $count);
+
+    // Test that bundle within admin toolbar appears in search.
+    $this->assertSuggestionContains('lola', 'admin/structure/media/manage/lola/fields');
+
+    // Assert that a link after the limit (10) doesn't appear in admin toolbar.
+    $toby_url = '/admin/structure/media/manage/toby/fields';
+    $assert_session->elementNotContains('css', '#toolbar-administration', $toby_url);
+
+    // Assert that a link excluded from admin toolbar appears in search.
+    $this->assertSuggestionContains('toby', $toby_url);
+
+    // Test that adding a new bundle updates the extra links loaded from
+    // admin_toolbar.search route.
+    $this->createMediaType('image', [
+      'id' => 'zuzu',
+      'label' => 'Zuzu',
+    ]);
+
+    $this->drupalGet('admin');
+    $assert_session->waitForElementVisible('css', $search_tray);
+    $this->assertSuggestionContains('zuzu', '/admin/structure/media/manage/zuzu/fields');
+
+    // Test that deleting a bundle updates the extra links loaded from
+    // admin_toolbar.search route.
+    $toby = MediaType::load('toby');
+    $toby->delete();
+
+    $this->getSession()->reload();
+    $assert_session->waitForElementVisible('css', $search_tray);
+    $this->assertSuggestionNotContains('toby', $toby_url);
+  }
+
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml
index 7b7728100e0203152229f058b6017ff81db1e8fa..89eedeafa06375c3937d5a6155f15eec2a9d7535 100644
--- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.info.yml
@@ -3,13 +3,12 @@ description: Adds menu links to the Admin Toolbar.
 package: Administration
 
 type: module
-core: 8.x
+core_version_requirement: ^8.8.0 || ^9.0
 
 dependencies:
   - admin_toolbar:admin_toolbar
-  - drupal:system (>=8.6)
 
-# Information added by Drupal.org packaging script on 2019-10-29
-version: '8.x-2.0'
+# Information added by Drupal.org packaging script on 2020-03-24
+version: '8.x-2.2'
 project: 'admin_toolbar'
-datestamp: 1572370993
+datestamp: 1585017182
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.install b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.install
new file mode 100644
index 0000000000000000000000000000000000000000..1cc14c73d3d9d84970e7f3beefcb04087de69cbf
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.install
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Admin Toolbar Tools module.
+ */
+
+/**
+ * Install the Admin Toolbar Search module.
+ */
+function admin_toolbar_tools_update_8001() {
+  // Installing the Admin Toolbar Search module.
+  \Drupal::service('module_installer')->install(['admin_toolbar_search']);
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml
index 567e32d16ec24eb534d728fd7c9a5f2462a343a3..670a4f76c6219fbffba857c4047554a17dfdf197 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.links.menu.yml
@@ -75,6 +75,12 @@ admin_toolbar_tools.flush_rendercache:
   parent: admin_toolbar_tools.flush
   menu_name: admin
 
+admin_toolbar_tools.theme_rebuild:
+  title: 'Rebuild theme registry'
+  route_name: admin_toolbar_tools.theme_rebuild
+  parent: admin_toolbar_tools.flush
+  menu_name: admin
+
 admin_toolbar_tools.extra_links:
   deriver: \Drupal\admin_toolbar_tools\Plugin\Derivative\ExtraLinks
   menu_name: admin
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module
index ec4e682fa2e917414223954a78dcadf08317f470..6958a862975878a06152f06af3a6503a9cce48b4 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.module
@@ -48,7 +48,7 @@ function admin_toolbar_tools_help($route_name, RouteMatchInterface $route_match)
  * Implements hook_entity_insert().
  */
 function admin_toolbar_tools_entity_insert(EntityInterface $entity) {
-  $entities = \Drupal::service('admin_toolbar_tools.search_links')->getRebuildEntityTypes();
+  $entities = \Drupal::service('admin_toolbar_tools.helper')->getRebuildEntityTypes();
   if (in_array($entity->getEntityTypeId(), $entities)) {
     \Drupal::service('plugin.manager.menu.link')->rebuild();
   }
@@ -58,7 +58,7 @@ function admin_toolbar_tools_entity_insert(EntityInterface $entity) {
  * Implements hook_entity_update().
  */
 function admin_toolbar_tools_entity_update(EntityInterface $entity) {
-  $entities = \Drupal::service('admin_toolbar_tools.search_links')->getRebuildEntityTypes();
+  $entities = \Drupal::service('admin_toolbar_tools.helper')->getRebuildEntityTypes();
   if (in_array($entity->getEntityTypeId(), $entities)) {
     \Drupal::service('plugin.manager.menu.link')->rebuild();
   }
@@ -68,7 +68,7 @@ function admin_toolbar_tools_entity_update(EntityInterface $entity) {
  * Implements hook_entity_delete().
  */
 function admin_toolbar_tools_entity_delete(EntityInterface $entity) {
-  $entities = \Drupal::service('admin_toolbar_tools.search_links')->getRebuildEntityTypes();
+  $entities = \Drupal::service('admin_toolbar_tools.helper')->getRebuildEntityTypes();
   if (in_array($entity->getEntityTypeId(), $entities)) {
     \Drupal::service('plugin.manager.menu.link')->rebuild();
   }
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml
index ee4eb23a2ccd04b88fe42ab9ab49b125701e365f..bcae9db1b7b37b7fa0eaf8c3d7b94c5f30c74bca 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.routing.yml
@@ -70,18 +70,20 @@ admin_toolbar_tools.flush_twig:
     _permission: 'administer site configuration'
     _csrf_token: 'TRUE'
 
-admin_toolbar.run.cron:
-  path: '/run-cron'
+admin_toolbar_tools.theme_rebuild:
+  path: '/admin/flush/theme_rebuild'
   defaults:
-    _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::runCron'
-    _title: 'Run cron'
+    _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::themeRebuild'
+    _title: 'Theme Rebuild'
   requirements:
     _permission: 'administer site configuration'
     _csrf_token: 'TRUE'
 
-admin_toolbar.search:
-  path: '/admin/admin-toolbar-search'
+admin_toolbar.run.cron:
+  path: '/run-cron'
   defaults:
-    _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::search'
+    _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::runCron'
+    _title: 'Run cron'
   requirements:
     _permission: 'administer site configuration'
+    _csrf_token: 'TRUE'
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml
index f772cffaae91fdab57c4f91726e8beba179c5b03..aa533c7871e4299d3f1b2aefe7941afe1a6eaca5 100644
--- a/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/admin_toolbar_tools.services.yml
@@ -1,9 +1,5 @@
 services:
-  admin_toolbar_tools.search_links:
-    class: Drupal\admin_toolbar_tools\SearchLinks
+  admin_toolbar_tools.helper:
+    class: Drupal\admin_toolbar_tools\AdminToolbarToolsHelper
     arguments:
      - '@entity_type.manager'
-     - '@module_handler'
-     - '@router.route_provider'
-     - '@cache_contexts_manager'
-     - '@cache.toolbar'
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/composer.json b/web/modules/admin_toolbar/admin_toolbar_tools/composer.json
index bb568793a3b735117d18e746c5e007830ed869da..fad275ff539e67a174f90e92ce76cc8c3dffe08c 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/composer.json
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/composer.json
@@ -26,7 +26,7 @@
       "name": "Mohamed Anis Taktak (matio89)",
       "homepage": "https://www.drupal.org/u/matio89",
       "role": "Maintainer"
-    }    
+    }
   ],
   "support": {
     "issues": "https://www.drupal.org/project/issues/admin_toolbar",
@@ -34,6 +34,6 @@
   },
   "require": {
     "drupal/admin_toolbar": "^2",
-    "drupal/core": "~8.7"
+    "drupal/core": "^8.8.0 || ^9.0"
   }
 }
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php b/web/modules/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php
new file mode 100644
index 0000000000000000000000000000000000000000..b270e679e9da56f4932030d70c0e253a40f096d9
--- /dev/null
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/src/AdminToolbarToolsHelper.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\admin_toolbar_tools;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Admin Toolbar Tools helper service.
+ */
+class AdminToolbarToolsHelper {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Create an AdminToolbarToolsHelper object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Gets a list of content entities.
+   *
+   * @return array
+   *   An array of metadata about content entities.
+   */
+  public function getBundleableEntitiesList() {
+    $entity_types = $this->entityTypeManager->getDefinitions();
+    $content_entities = [];
+    foreach ($entity_types as $key => $entity_type) {
+      if ($entity_type->getBundleEntityType() && ($entity_type->get('field_ui_base_route') != '')) {
+        $content_entities[$key] = [
+          'content_entity' => $key,
+          'content_entity_bundle' => $entity_type->getBundleEntityType(),
+        ];
+      }
+    }
+    return $content_entities;
+  }
+
+  /**
+   * Gets an array of entity types that should trigger a menu rebuild.
+   *
+   * @return array
+   *   An array of entity machine names.
+   */
+  public function getRebuildEntityTypes() {
+    $types = ['menu'];
+    $content_entities = $this->getBundleableEntitiesList();
+    $types = array_merge($types, array_column($content_entities, 'content_entity_bundle'));
+    return $types;
+  }
+
+}
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php b/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php
index 7677f2af67604bb8359089c50e52ca4c2cb12d5b..22ccded1fcf6600e3bd414f562d92effa89a61e7 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/src/Controller/ToolbarController.php
@@ -2,21 +2,20 @@
 
 namespace Drupal\admin_toolbar_tools\Controller;
 
-use Drupal\admin_toolbar_tools\SearchLinks;
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\CronInterface;
-use Drupal\Core\Menu\ContextualLinkManagerInterface;
-use Drupal\Core\Menu\LocalActionManagerInterface;
-use Drupal\Core\Menu\LocalTaskManagerInterface;
+use Drupal\Core\Menu\ContextualLinkManager;
+use Drupal\Core\Menu\LocalActionManager;
+use Drupal\Core\Menu\LocalTaskManager;
 use Drupal\Core\Menu\MenuLinkManagerInterface;
 use Drupal\Core\Plugin\CachedDiscoveryClearerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Drupal\Core\Template\TwigEnvironment;
+use Drupal\Core\Theme\Registry;
 
 /**
  * Class ToolbarController.
@@ -42,21 +41,21 @@ class ToolbarController extends ControllerBase {
   /**
    * A context link manager instance.
    *
-   * @var \Drupal\Core\Menu\ContextualLinkManagerInterface
+   * @var \Drupal\Core\Menu\ContextualLinkManager
    */
   protected $contextualLinkManager;
 
   /**
    * A local task manager instance.
    *
-   * @var \Drupal\Core\Menu\LocalTaskManagerInterface
+   * @var \Drupal\Core\Menu\LocalTaskManager
    */
   protected $localTaskLinkManager;
 
   /**
    * A local action manager instance.
    *
-   * @var \Drupal\Core\Menu\LocalActionManagerInterface
+   * @var \Drupal\Core\Menu\LocalActionManager
    */
   protected $localActionLinkManager;
 
@@ -103,11 +102,11 @@ class ToolbarController extends ControllerBase {
   protected $twig;
 
   /**
-   * The search links service.
+   * The search theme.registry service.
    *
-   * @var \Drupal\admin_toolbar_tools\SearchLinks
+   * @var \Drupal\Core\Theme\Registry
    */
-  protected $links;
+  protected $themeRegistry;
 
   /**
    * Constructs a ToolbarController object.
@@ -116,11 +115,11 @@ class ToolbarController extends ControllerBase {
    *   A cron instance.
    * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
    *   A menu link manager instance.
-   * @param \Drupal\Core\Menu\ContextualLinkManagerInterface $contextualLinkManager
+   * @param \Drupal\Core\Menu\ContextualLinkManager $contextualLinkManager
    *   A context link manager instance.
-   * @param \Drupal\Core\Menu\LocalTaskManagerInterface $localTaskLinkManager
+   * @param \Drupal\Core\Menu\LocalTaskManager $localTaskLinkManager
    *   A local task manager instance.
-   * @param \Drupal\Core\Menu\LocalActionManagerInterface $localActionLinkManager
+   * @param \Drupal\Core\Menu\LocalActionManager $localActionLinkManager
    *   A local action manager instance.
    * @param \Drupal\Core\Cache\CacheBackendInterface $cacheRender
    *   A cache backend interface instance.
@@ -134,21 +133,23 @@ class ToolbarController extends ControllerBase {
    *   A cache menu instance.
    * @param \Drupal\Core\Template\TwigEnvironment $twig
    *   A TwigEnvironment instance.
-   * @param \Drupal\admin_toolbar_tools\SearchLinks $links
-   *   The search links service.
+   * @param \Drupal\Core\Theme\Registry $theme_registry
+   *   The theme.registry service.
    */
-  public function __construct(CronInterface $cron,
-                              MenuLinkManagerInterface $menuLinkManager,
-                              ContextualLinkManagerInterface $contextualLinkManager,
-                              LocalTaskManagerInterface $localTaskLinkManager,
-                              LocalActionManagerInterface $localActionLinkManager,
-                              CacheBackendInterface $cacheRender,
-                              TimeInterface $time,
-                              RequestStack $request_stack,
-                              CachedDiscoveryClearerInterface $plugin_cache_clearer,
-                              CacheBackendInterface $cache_menu,
-                              TwigEnvironment $twig,
-                              SearchLinks $links) {
+  public function __construct(
+    CronInterface $cron,
+    MenuLinkManagerInterface $menuLinkManager,
+    ContextualLinkManager $contextualLinkManager,
+    LocalTaskManager $localTaskLinkManager,
+    LocalActionManager $localActionLinkManager,
+    CacheBackendInterface $cacheRender,
+    TimeInterface $time,
+    RequestStack $request_stack,
+    CachedDiscoveryClearerInterface $plugin_cache_clearer,
+    CacheBackendInterface $cache_menu,
+    TwigEnvironment $twig,
+    Registry $theme_registry
+  ) {
     $this->cron = $cron;
     $this->menuLinkManager = $menuLinkManager;
     $this->contextualLinkManager = $contextualLinkManager;
@@ -160,7 +161,7 @@ public function __construct(CronInterface $cron,
     $this->pluginCacheClearer = $plugin_cache_clearer;
     $this->cacheMenu = $cache_menu;
     $this->twig = $twig;
-    $this->links = $links;
+    $this->themeRegistry = $theme_registry;
   }
 
   /**
@@ -179,7 +180,7 @@ public static function create(ContainerInterface $container) {
       $container->get('plugin.cache_clearer'),
       $container->get('cache.menu'),
       $container->get('twig'),
-      $container->get('admin_toolbar_tools.search_links')
+      $container->get('theme.registry')
     );
   }
 
@@ -283,10 +284,12 @@ public function cacheRender() {
   }
 
   /**
-   * Return additional search links.
+   * Rebuild the theme registry.
    */
-  public function search() {
-    return new JsonResponse($this->links->getLinks());
+  public function themeRebuild() {
+    $this->themeRegistry->reset();
+    $this->messenger()->addMessage($this->t('Theme registry rebuilded.'));
+    return new RedirectResponse($this->reloadPage());
   }
 
 }
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php b/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php
index c667b6b38b1ef36ce7f1867821db8fae037f7849..e3d62177c44774edb0f35929cdd86ee1189901cf 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/src/Plugin/Derivative/ExtraLinks.php
@@ -52,7 +52,7 @@ class ExtraLinks extends DeriverBase implements ContainerDeriverInterface {
   /**
    * {@inheritdoc}
    */
-  public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, ThemeHandlerInterface $theme_handler) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, ThemeHandlerInterface $theme_handler) {
     $this->entityTypeManager = $entity_type_manager;
     $this->moduleHandler = $module_handler;
     $this->routeProvider = $route_provider;
@@ -64,7 +64,6 @@ public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_
    */
   public static function create(ContainerInterface $container, $base_plugin_id) {
     return new static(
-      $base_plugin_id,
       $container->get('entity_type.manager'),
       $container->get('module_handler'),
       $container->get('router.route_provider'),
@@ -96,7 +95,7 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       $content_entity_bundle_storage = $this->entityTypeManager->getStorage($content_entity_bundle);
       $bundles_ids = $content_entity_bundle_storage->getQuery()->pager(self::MAX_BUNDLE_NUMBER)->execute();
       $bundles = $this->entityTypeManager->getStorage($content_entity_bundle)->loadMultiple($bundles_ids);
-      if (count($bundles) == self::MAX_BUNDLE_NUMBER) {
+      if (count($bundles) == self::MAX_BUNDLE_NUMBER && $this->routeExists('entity.' . $content_entity_bundle . '.collection')) {
         $links[$content_entity_bundle . '.collection'] = [
           'title' => $this->t('All types'),
           'route_name' => 'entity.' . $content_entity_bundle . '.collection',
@@ -130,8 +129,8 @@ public function getDerivativeDefinitions($base_plugin_definition) {
             $content_entity_bundle_root = $key;
           }
           else {
-            $links[$key]['parent'] = $content_entity_bundle_root;
-            $links[$key]['title'] = t('Edit');
+            $links[$key]['parent'] = $base_plugin_definition['id'] . ':' . $content_entity_bundle_root;
+            $links[$key]['title'] = $this->t('Edit');
           }
         }
         if ($this->moduleHandler->moduleExists('field_ui')) {
@@ -191,23 +190,23 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       'parent' => 'entity.user.collection',
     ] + $base_plugin_definition;
     $links['user.admin_permissions'] = [
-      'title' => t('Permissions'),
+      'title' => $this->t('Permissions'),
       'route_name' => 'user.admin_permissions',
       'parent' => 'entity.user.collection',
     ] + $base_plugin_definition;
     $links['entity.user_role.collection'] = [
-      'title' => t('Roles'),
+      'title' => $this->t('Roles'),
       'route_name' => 'entity.user_role.collection',
       'parent' => 'entity.user.collection',
     ] + $base_plugin_definition;
     $links['user.logout'] = [
-      'title' => t('Logout'),
+      'title' => $this->t('Logout'),
       'route_name' => 'user.logout',
       'parent' => 'admin_toolbar_tools.help',
       'weight' => 10,
     ] + $base_plugin_definition;
     $links['user.role_add'] = [
-      'title' => t('Add role'),
+      'title' => $this->t('Add role'),
       'route_name' => 'user.role_add',
       'parent' => $base_plugin_definition['id'] . ':entity.user_role.collection',
       'weight' => -5,
diff --git a/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php b/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php
index 94c27cd59e9758c412a14e283fc1145fe14e0619..9669a49a5f06eaed8aad6ef1dd6481a1914b5836 100755
--- a/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php
+++ b/web/modules/admin_toolbar/admin_toolbar_tools/tests/src/Functional/AdminToolbarToolsAlterTest.php
@@ -22,6 +22,11 @@ class AdminToolbarToolsAlterTest extends BrowserTestBase {
     'admin_toolbar_tools',
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * A test user with permission to access the administrative toolbar.
    *
diff --git a/web/modules/admin_toolbar/composer.json b/web/modules/admin_toolbar/composer.json
index a97caac9b0186101e747fab92f10ea2a0b3bf115..650ed769fbab1240e04874011ee85945d5b61948 100755
--- a/web/modules/admin_toolbar/composer.json
+++ b/web/modules/admin_toolbar/composer.json
@@ -26,10 +26,13 @@
       "name": "Mohamed Anis Taktak (matio89)",
       "homepage": "https://www.drupal.org/u/matio89",
       "role": "Maintainer"
-    }    
+    }
   ],
   "support": {
     "issues": "https://www.drupal.org/project/issues/admin_toolbar",
     "source": "http://cgit.drupalcode.org/admin_toolbar"
+  },
+  "require": {
+    "drupal/core": "^8.8.0 || ^9.0"
   }
 }
diff --git a/web/modules/admin_toolbar/src/Render/Element/AdminToolbar.php b/web/modules/admin_toolbar/src/Render/Element/AdminToolbar.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d0000011f2c8c88469c1ecb6405496a3c364bf4
--- /dev/null
+++ b/web/modules/admin_toolbar/src/Render/Element/AdminToolbar.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\admin_toolbar\Render\Element;
+
+use Drupal\Core\Menu\MenuTreeParameters;
+use Drupal\Core\Security\TrustedCallbackInterface;
+
+/**
+ * Class AdminToolbar.
+ *
+ * @package Drupal\admin_toolbar\Render\Element
+ */
+class AdminToolbar implements TrustedCallbackInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks() {
+    return ['preRenderTray'];
+  }
+
+  /**
+   * Renders the toolbar's administration tray.
+   *
+   * This is a clone of core's toolbar_prerender_toolbar_administration_tray()
+   * function, which uses setMaxDepth(4) instead of setTopLevelOnly().
+   *
+   * @param array $build
+   *   A renderable array.
+   *
+   * @return array
+   *   The updated renderable array.
+   *
+   * @see toolbar_prerender_toolbar_administration_tray()
+   */
+  public static function preRenderTray(array $build) {
+    $menu_tree = \Drupal::service('toolbar.menu_tree');
+    $parameters = new MenuTreeParameters();
+    $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(4)->onlyEnabledLinks();
+    $tree = $menu_tree->load(NULL, $parameters);
+    $manipulators = [
+      ['callable' => 'menu.default_tree_manipulators:checkAccess'],
+      ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
+      ['callable' => 'toolbar_tools_menu_navigation_links'],
+    ];
+    $tree = $menu_tree->transform($tree, $manipulators);
+    $build['administration_menu'] = $menu_tree->build($tree);
+    return $build;
+  }
+
+}
diff --git a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php
index bd57d502b3be60bddd39cb75a1eef9c308a3e1cf..c17f575ee5135a42040e5336a096d0a855a500d3 100755
--- a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php
+++ b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarAlterTest.php
@@ -22,6 +22,11 @@ class AdminToolbarAlterTest extends BrowserTestBase {
     'admin_toolbar',
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * A test user with permission to access the administrative toolbar.
    *
diff --git a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php
index 88a6261b1c300ab6f9c0a00a31e37061072acdaa..8da6ca9ae27324dc9341d4152bc94910b2ff1af4 100644
--- a/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php
+++ b/web/modules/admin_toolbar/tests/src/Functional/AdminToolbarToolsSortTest.php
@@ -28,6 +28,11 @@ class AdminToolbarToolsSortTest extends BrowserTestBase {
     'field_ui',
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * A test user with permission to access the administrative toolbar.
    *
@@ -35,13 +40,6 @@ class AdminToolbarToolsSortTest extends BrowserTestBase {
    */
   protected $adminUser;
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-  }
-
   /**
    * Tests that menu updates on entity add/update/delete.
    */
diff --git a/web/modules/admin_toolbar/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php b/web/modules/admin_toolbar/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php
deleted file mode 100755
index 5bd143f47050769db35c0a25dbf34f2faababe99..0000000000000000000000000000000000000000
--- a/web/modules/admin_toolbar/tests/src/FunctionalJavascript/AdminToolbarSearchTest.php
+++ /dev/null
@@ -1,339 +0,0 @@
-<?php
-
-namespace Drupal\Tests\admin_toolbar\FunctionalJavascript;
-
-use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
-use Drupal\media\Entity\MediaType;
-use Drupal\system\Entity\Menu;
-use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
-
-/**
- * Test the functionality of admin toolbar search.
- *
- * @group admin_toolbar
- */
-class AdminToolbarSearchTest extends WebDriverTestBase {
-
-  use MediaTypeCreationTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = [
-    'admin_toolbar',
-    'admin_toolbar_tools',
-    'node',
-    'media',
-    'field_ui',
-    'menu_ui',
-    'block',
-  ];
-
-  /**
-   * The admin user for tests.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $adminUser;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    $this->drupalCreateContentType([
-      'type' => 'article',
-      'name' => 'Article',
-    ]);
-
-    $dog_names = [
-      'archie' => 'Archie',
-      'bailey' => 'Bailey',
-      'bella' => 'Bella',
-      'buddy' => 'Buddy',
-      'charlie' => 'Charlie',
-      'coco' => 'Coco',
-      'daisy' => 'Daisy',
-      'frankie' => 'Frankie',
-      'jack' => 'Jack',
-      'lola' => 'Lola',
-      'lucy' => 'Lucy',
-      'max' => 'Max',
-      'milo' => 'Milo',
-      'molly' => 'Molly',
-      'ollie' => 'Ollie',
-      'oscar' => 'Oscar',
-      'rosie' => 'Rosie',
-      'ruby' => 'Ruby',
-      'teddy' => 'Teddy',
-      'toby' => 'Toby',
-    ];
-
-    foreach ($dog_names as $machine_name => $label) {
-      $this->createMediaType('image', [
-        'id' => $machine_name,
-        'label' => $label,
-      ]);
-    }
-
-    $baby_names = [
-      'ada' => 'Ada',
-      'amara' => 'Amara',
-      'amelia' => 'Amelia',
-      'arabella' => 'Arabella',
-      'asher' => 'Asher',
-      'astrid' => 'Astrid',
-      'atticus' => 'Atticus',
-      'aurora' => 'Aurora',
-      'ava' => 'Ava',
-      'cora' => 'Cora',
-      'eleanor' => 'Eleanor',
-      'eloise' => 'Eloise',
-      'felix' => 'Felix',
-      'freya' => 'Freya',
-      'genevieve' => 'Genevieve',
-      'isla' => 'Isla',
-      'jasper' => 'Jasper',
-      'luna' => 'Luna',
-      'maeve' => 'Maeve',
-      'milo' => 'Milo',
-      'nora' => 'Nora',
-      'olivia' => 'Olivia',
-      'ophelia' => 'Ophelia',
-      'posie' => 'Posie',
-      'rose' => 'Rose',
-      'silas' => 'Silas',
-      'soren' => 'Soren',
-    ];
-
-    foreach ($baby_names as $id => $label) {
-      $menu = Menu::create([
-        'id' => $id,
-        'label' => $label,
-      ]);
-      $menu->save();
-    }
-
-    $this->drupalPlaceBlock('local_tasks_block');
-
-    $this->adminUser = $this->drupalCreateUser([
-      'access toolbar',
-      'administer menu',
-      'access administration pages',
-      'administer site configuration',
-      'administer content types',
-      'administer node fields',
-      'access media overview',
-      'administer media',
-      'administer media fields',
-      'administer media form display',
-      'administer media display',
-      'administer media types',
-    ]);
-  }
-
-  /**
-   * Tests search functionality.
-   */
-  public function testSearchFunctionality() {
-
-    $search_tab = '#toolbar-item-administration-search';
-    $search_tray = '#toolbar-item-administration-search-tray';
-
-    $this->drupalLogin($this->adminUser);
-    $this->assertSession()->responseContains('admin.toolbar_search.css');
-    $this->assertSession()->responseContains('admin_toolbar_search.js');
-    $this->assertSession()->waitForElementVisible('css', $search_tab)->click();
-    $this->assertSession()->waitForElementVisible('css', $search_tray);
-
-    $this->assertSuggestionContains('basic', 'admin/config/system/site-information');
-
-    // Rebuild menu items.
-    drupal_flush_all_caches();
-
-    // Test that the route admin_toolbar.search returns expected json.
-    $this->drupalGet('/admin/admin-toolbar-search');
-
-    $search_menus = [
-      'cora',
-      'eleanor',
-      'eloise',
-      'felix',
-      'freya',
-      'genevieve',
-      'isla',
-      'jasper',
-      'luna',
-      'maeve',
-      'milo',
-      'nora',
-      'olivia',
-      'ophelia',
-      'posie',
-      'rose',
-      'silas',
-      'soren',
-    ];
-
-    $toolbar_menus = [
-      'ada',
-      'amara',
-      'amelia',
-      'arabella',
-      'asher',
-      'astrid',
-      'atticus',
-      'aurora',
-      'ava',
-    ];
-
-    foreach ($search_menus as $menu_id) {
-      $this->assertSession()->responseContains('\/admin\/structure\/menu\/manage\/' . $menu_id);
-    }
-
-    foreach ($toolbar_menus as $menu_id) {
-      $this->assertSession()->responseNotContains('\/admin\/structure\/menu\/manage\/' . $menu_id);
-    }
-
-    $this->drupalGet('/admin');
-
-    foreach ($search_menus as $menu_id) {
-      $this->assertMenuDoesNotHaveHref('/admin/structure/menu/manage/' . $menu_id);
-    }
-
-    foreach ($toolbar_menus as $menu_id) {
-      $this->assertMenuHasHref('/admin/structure/menu/manage/' . $menu_id);
-    }
-
-    $this->drupalGet('admin/structure/types/manage/article/fields');
-    $this->assertSession()->waitForElementVisible('css', $search_tray);
-
-    $this->assertSuggestionContains('article manage fields', '/admin/structure/types/manage/article/fields');
-
-    $suggestions = $this->assertSession()
-      ->waitForElementVisible('css', 'ul.ui-autocomplete');
-
-    // Assert there is only one suggestion with a link to
-    // /admin/structure/types/manage/article/fields.
-    $count = count($suggestions->findAll('xpath', '//span[contains(text(), "/admin/structure/types/manage/article/fields")]'));
-    $this->assertEquals(1, $count);
-
-    // Test that bundle within admin toolbar appears in search.
-    $this->assertSuggestionContains('lola', 'admin/structure/media/manage/lola/fields');
-
-    // Assert that a link after the limit (10) doesn't appear in admin toolbar.
-    $toby_url = '/admin/structure/media/manage/toby/fields';
-    $this->assertSession()
-      ->elementNotContains('css', '#toolbar-administration', $toby_url);
-
-    // Assert that a link excluded from admin toolbar appears in search.
-    $this->assertSuggestionContains('toby', $toby_url);
-
-    // Test that adding a new bundle updates the extra links loaded from
-    // admin_toolbar.search route.
-    $this->createMediaType('image', [
-      'id' => 'zuzu',
-      'label' => 'Zuzu',
-    ]);
-
-    $this->drupalGet('admin');
-    $this->assertSession()->waitForElementVisible('css', $search_tray);
-    $this->assertSuggestionContains('zuzu', '/admin/structure/media/manage/zuzu/fields');
-
-    // Test that deleting a bundle updates the extra links loaded from
-    // admin_toolbar.search route.
-    $toby = MediaType::load('toby');
-    $toby->delete();
-
-    $this->getSession()->reload();
-    $this->assertSession()->waitForElementVisible('css', $search_tray);
-    $this->assertSuggestionNotContains('toby', $toby_url);
-
-  }
-
-  /**
-   * Assert that the search suggestions contain a given string with given input.
-   *
-   * @param string $search
-   *   The string to search for.
-   * @param string $contains
-   *   Some HTML that is expected to be within the suggestions element.
-   */
-  protected function assertSuggestionContains($search, $contains) {
-    $this->resetSearch();
-    $page = $this->getSession()->getPage();
-    $page->fillField('admin-toolbar-search-input', $search);
-    $page->waitFor(3, function () use ($page) {
-      return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE);
-    });
-    $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml();
-    $this->assertContains($contains, $suggestions_markup);
-  }
-
-  /**
-   * Assert that the search suggestions does not contain a given string.
-   *
-   * Assert that the search suggestions does not contain a given string with a
-   * given input.
-   *
-   * @param string $search
-   *   The string to search for.
-   * @param string $contains
-   *   Some HTML that is not expected to be within the suggestions element.
-   */
-  protected function assertSuggestionNotContains($search, $contains) {
-    $this->resetSearch();
-    $page = $this->getSession()->getPage();
-    $page->fillField('admin-toolbar-search-input', $search);
-    $page->waitFor(3, function () use ($page) {
-      return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === TRUE);
-    });
-    if ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE) {
-      return;
-    }
-    else {
-      $suggestions_markup = $page->find('css', 'ul.ui-autocomplete')->getHtml();
-      $this->assertNotContains($contains, $suggestions_markup);
-    }
-  }
-
-  /**
-   * Search for an empty string to clear out the autocomplete suggestions.
-   */
-  protected function resetSearch() {
-    $page = $this->getSession()->getPage();
-    // Empty out the suggestions.
-    $page->fillField('admin-toolbar-search-input', '');
-    $page->waitFor(3, function () use ($page) {
-      return ($page->find('css', 'ul.ui-autocomplete')->isVisible() === FALSE);
-    });
-  }
-
-  /**
-   * Checks that there is a link with the specified url in the admin toolbar.
-   *
-   * @param string $url
-   *   The url to assert exists in the admin menu.
-   *
-   * @throws \Behat\Mink\Exception\ElementNotFoundException
-   */
-  protected function assertMenuHasHref($url) {
-    $this->assertSession()
-      ->elementExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]');
-  }
-
-  /**
-   * Checks that there is no link with the specified url in the admin toolbar.
-   *
-   * @param string $url
-   *   The url to assert exists in the admin menu.
-   *
-   * @throws \Behat\Mink\Exception\ExpectationException
-   */
-  protected function assertMenuDoesNotHaveHref($url) {
-    $this->assertSession()
-      ->elementNotExists('xpath', '//div[@id="toolbar-item-administration-tray"]//a[contains(@href, "' . $url . '")]');
-  }
-
-}
diff --git a/web/modules/entity_reference_revisions/composer.json b/web/modules/entity_reference_revisions/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..7fdfb2e91999c1c74d5a33474ad0cc79a209f845
--- /dev/null
+++ b/web/modules/entity_reference_revisions/composer.json
@@ -0,0 +1,12 @@
+{
+  "name": "drupal/entity_reference_revisions",
+  "description": "Entity Reference Revisions",
+  "type": "drupal-module",
+  "license": "GPL-2.0",
+  "require": {
+    "drupal/core": "^8.7.7 || ^9"
+  },
+  "require-dev": {
+    "drupal/diff": "1.x-dev"
+  }
+}
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml
index 6c425b7f118b1ddc550a5275aa8a6ac8c8975f8e..55807d43482d557361720824c7805204112f28af 100644
--- a/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml
@@ -1,14 +1,10 @@
 name: Entity Reference Revisions
 type: module
 description: Adds a Entity Reference field type with revision support.
-# core: 8.x
+core_version_requirement: ^8.7.7 || ^9
 package: Field types
 
-test_dependencies:
-  - diff:diff
-
-# Information added by Drupal.org packaging script on 2017-05-26
-version: '8.x-1.3'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-03-11
+version: '8.x-1.8'
 project: 'entity_reference_revisions'
-datestamp: 1495814304
+datestamp: 1583961849
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml
new file mode 100644
index 0000000000000000000000000000000000000000..beb8161771ec246b11b1d3c12f3db09d65377197
--- /dev/null
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml
@@ -0,0 +1,6 @@
+entity_reference_revisions.delete_orphans:
+  title: 'Delete orphaned composite entities'
+  parent: system.admin_config_system
+  description: 'Delete revisions of entities that are no longer used in Entity Reference Revisions fields.'
+  route_name: entity_reference_revisions.delete_orphans
+  weight: 30
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.module b/web/modules/entity_reference_revisions/entity_reference_revisions.module
index ba9ac496f96fb8eb7cf439eb0c618205f66b0aea..fa24f77eab5fe6092919d721786c36778a016370 100644
--- a/web/modules/entity_reference_revisions/entity_reference_revisions.module
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.module
@@ -6,9 +6,14 @@
  */
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\TranslatableRevisionableStorageInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\FieldStorageConfigInterface;
@@ -218,3 +223,129 @@ function entity_reference_revisions_form_field_ui_field_storage_add_form_alter(a
   unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions']);
   $form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions'] = t('Other…');
 }
+
+/**
+ * Implements hook_entity_revision_create().
+ */
+function entity_reference_revisions_entity_revision_create(ContentEntityInterface $new_revision, ContentEntityInterface $entity, $keep_untranslatable_fields) {
+  $entity_type_manager = \Drupal::entityTypeManager();
+  $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
+  foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
+    if ($field_definition->getType() == 'entity_reference_revisions' && !$field_definition->isTranslatable()) {
+      $target_entity_type_id = $field_definition->getSetting('target_type');
+      if ($entity_type_manager->getDefinition($target_entity_type_id)->get('entity_revision_parent_id_field')) {
+
+        // The default implementation copied the values from the current
+        // default revision into the field since it is not translatable.
+        // Take the originally referenced entity, create a new revision
+        // of it and set that instead on the new entity revision.
+        $active_langcode = $entity->language()->getId();
+        $target_storage = \Drupal::entityTypeManager()->getStorage($target_entity_type_id);
+        if ($target_storage instanceof TranslatableRevisionableStorageInterface) {
+
+          $items = $entity->get($field_name);
+          $translation_items = NULL;
+          if (!$new_revision->isDefaultTranslation() && $storage instanceof TranslatableRevisionableStorageInterface) {
+            $translation_items = $items;
+            $items = $storage->load($new_revision->id())->get($field_name);
+          }
+
+          $values = [];
+          foreach ($items as $delta => $item) {
+            // If the target entity is missing, let's skip it.
+            if (empty($item->entity)) {
+              continue;
+            }
+
+            // Use the item from the translation if it exists.
+            // If we have translation items, use that if one with the matching
+            // target id exists.
+            if ($translation_items) {
+              foreach ($translation_items as $translation_item) {
+                if ($item->target_id == $translation_item->target_id) {
+                  $item = $translation_item;
+                  break;
+                }
+              }
+            }
+
+            /** @var \Drupal\Core\Entity\ContentEntityInterface $target_entity */
+            $target_entity = $item->entity;
+            if (!$target_entity->hasTranslation($active_langcode)) {
+              $target_entity->addTranslation($active_langcode, $target_entity->toArray());
+            }
+            $target_entity = $item->entity->getTranslation($active_langcode);
+            $revised_entity = $target_storage->createRevision($target_entity, $new_revision->isDefaultRevision(), $keep_untranslatable_fields);
+
+            // Restore the revision ID.
+            $revision_key = $revised_entity->getEntityType()->getKey('revision');
+            $revised_entity->set($revision_key, $revised_entity->getLoadedRevisionId());
+            $values[$delta] = $revised_entity;
+          }
+          $new_revision->set($field_name, $values);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Batch callback to dispatch the orphan composite batch operation to a service.
+ */
+function _entity_reference_revisions_orphan_purger_batch_dispatcher() {
+  $args = func_get_args();
+  list($service, $method) = explode(':', array_shift($args));
+  // The second argument (context) is passed by reference.
+  $values = $args[1];
+  $args[1] = &$values;
+  call_user_func_array([\Drupal::service($service), $method], $args);
+}
+
+/**
+ * Implements hook_entity_delete().
+ *
+ * Performs garbage collection for composite entities that were not removed
+ * by EntityReferenceRevisionsItem.
+ */
+function entity_reference_revisions_entity_delete(EntityInterface $entity) {
+  if (!$entity instanceof FieldableEntityInterface) {
+    return;
+  }
+
+  $entity_type_manager = \Drupal::entityTypeManager();
+  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
+  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
+  foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
+    $field_class = $field_type_manager->getPluginClass($field_definition->getType());
+    if ($field_class == EntityReferenceRevisionsItem::class || is_subclass_of($field_class, EntityReferenceRevisionsItem::class)) {
+      $target_entity_type_id = $field_definition->getSetting('target_type');
+      $target_entity_storage = $entity_type_manager->getStorage($target_entity_type_id);
+      $target_entity_type = $target_entity_storage->getEntityType();
+
+      $parent_type_field = $target_entity_type->get('entity_revision_parent_type_field');
+      $parent_id_field = $target_entity_type->get('entity_revision_parent_id_field');
+      $parent_name_field = $target_entity_type->get('entity_revision_parent_field_name_field');
+
+      if ($parent_type_field && $parent_id_field && $parent_name_field) {
+        $entity_ids = $target_entity_storage
+          ->getQuery()
+          ->allRevisions()
+          ->condition($parent_type_field, $entity->getEntityTypeId())
+          ->condition($parent_id_field, $entity->id())
+          ->condition($parent_name_field, $field_name)
+          ->execute();
+
+        if (empty($entity_ids)) {
+          continue;
+        }
+        $entity_ids = array_unique($entity_ids);
+        foreach ($entity_ids as $revision_id => $entity_id) {
+          \Drupal::queue('entity_reference_revisions_orphan_purger')->createItem([
+            'entity_id' => $entity_id,
+            'entity_type_id' => $target_entity_type_id,
+          ]);
+        }
+      }
+    }
+  }
+}
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bcd64c16d34741ee2c09057aadbc4e5e5b8349cf
--- /dev/null
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml
@@ -0,0 +1,3 @@
+delete orphan revisions:
+  title: 'Delete orphan revisions'
+  description: 'Allow to access to the Entity Reference Revisions orphan deletion form.'
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cd90db817dfa6418cefe7cb5c2dbfb8f5eea3062
--- /dev/null
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml
@@ -0,0 +1,7 @@
+entity_reference_revisions.delete_orphans:
+  path: '/admin/config/system/delete-orphans'
+  defaults:
+    _form: 'Drupal\entity_reference_revisions\Form\OrphanedCompositeEntitiesDeleteForm'
+    _title: 'Delete orphaned composite entities'
+  requirements:
+    _permission: 'delete orphan revisions'
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml
new file mode 100755
index 0000000000000000000000000000000000000000..d112df982e4c4830e49666f570e9819ffadb4056
--- /dev/null
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml
@@ -0,0 +1,4 @@
+services:
+  entity_reference_revisions.orphan_purger:
+    class: Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger
+    arguments: ['@entity_type.manager', '@date.formatter', '@datetime.time', '@database', '@messenger']
diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc b/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc
index 9d351b3765b5ccf7b1483aaa26d9941eba52af5d..6dd8ec66f71ade9143dc80931a407ab0ec505680 100644
--- a/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc
+++ b/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc
@@ -43,7 +43,7 @@ function entity_reference_revisions_field_views_data(FieldStorageConfigInterface
     // Provide a reverse relationship for the entity type that is referenced by
     // the field.
     $args['@entity'] = $entity_type->getLabel();
-    $args['@label'] = $target_entity_type->getLowercaseLabel();
+    $args['@label'] = $target_entity_type->getSingularLabel();
     $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name;
     /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
     $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
diff --git a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php
index ba907277add9b397cae852212a1535035c1508e1..983e7be2697e036570f1756cf5a9795bedb3d12a 100644
--- a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php
+++ b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php
@@ -3,6 +3,8 @@
 namespace Drupal\entity_reference_revisions;
 
 use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldItemListTranslationChangesInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\EntityReferenceFieldItemList;
@@ -122,4 +124,28 @@ public function defaultValuesFormSubmit(array $element, array &$form, FormStateI
     return $default_value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasAffectingChanges(FieldItemListInterface $original_items, $langcode) {
+    // If there are fewer items, then it is a change.
+    if (count($this) < count($original_items)) {
+      return TRUE;
+    }
+
+    foreach ($this as $delta => $item) {
+      // If this is a different entity, then it is an affecting change.
+      if (!$original_items->offsetExists($delta) || $item->target_id != $original_items[$delta]->target_id) {
+        return TRUE;
+      }
+      // If it is the same entity, only consider it as having affecting changes
+      // if the target entity itself has changes.
+      if ($item->entity && $item->entity->hasTranslation($langcode) && $item->entity->getTranslation($langcode)->hasTranslationChanges()) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
 }
diff --git a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php
new file mode 100644
index 0000000000000000000000000000000000000000..8735817fc013be61a37c740895dfaa743b836d46
--- /dev/null
+++ b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php
@@ -0,0 +1,370 @@
+<?php
+
+namespace Drupal\entity_reference_revisions;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Manages orphan composite revision deletion.
+ */
+class EntityReferenceRevisionsOrphanPurger {
+
+  use StringTranslationTrait;
+
+  /**
+   * Parent is valid.
+   */
+  const PARENT_VALID = 0;
+
+  /**
+   * Parent is invalid and usage can not be verified.
+   */
+  const PARENT_INVALID_SKIP = 1;
+
+  /**
+   * Parent is invalid and paragraph is safe to delete.
+   */
+  const PARENT_INVALID_DELETE = 2;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The database service.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * List of already checked parents.
+   *
+   * @var bool[][]
+   */
+  protected $validParents = [];
+
+  /**
+   * Constructs a EntityReferenceRevisionsOrphanManager object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database service.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, DateFormatterInterface $date_formatter, TimeInterface $time, Connection $database, MessengerInterface $messenger) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->dateFormatter = $date_formatter;
+    $this->time = $time;
+    $this->database = $database;
+    $this->messenger = $messenger;
+  }
+
+  /**
+   * Deletes unused revision or an entity if there are no revisions remaining.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision
+   *   The composite revision.
+   *
+   * @return bool
+   *   TRUE if an entity revision was deleted. Otherwise, FALSE.
+   */
+  public function deleteUnusedRevision(ContentEntityInterface $composite_revision) {
+    // If this is the default revision of the composite entity, check if there
+    // are other revisions. If there are not, delete the composite entity.
+    $composite_storage = $this->entityTypeManager->getStorage($composite_revision->getEntityTypeId());
+
+    if ($composite_revision->isDefaultRevision()) {
+      $count = $composite_storage
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->allRevisions()
+        ->condition($composite_storage->getEntityType()->getKey('id'), $composite_revision->id())
+        ->count()
+        ->execute();
+      if ($count <= 1) {
+        $composite_revision->delete();
+        return TRUE;
+      }
+    }
+    else {
+      // Delete the revision if this is not the default one.
+      $composite_storage->deleteRevision($composite_revision->getRevisionId());
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Batch operation for checking orphans for a given entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type id, for example 'paragraph'.
+   * @param array $context
+   *   The context array.
+   */
+  public function deleteOrphansBatchOperation($entity_type_id, array &$context) {
+    $composite_type = $this->entityTypeManager->getDefinition($entity_type_id);
+    $composite_revision_key = $composite_type->getKey('revision');
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $composite_storage */
+    $composite_storage = $this->entityTypeManager->getStorage($entity_type_id);
+    $batch_size = Settings::get('entity_update_batch_size', 50);
+
+    if (empty($context['sandbox']['total'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_revision_id'] = -1;
+      $context['sandbox']['total'] = (int) $composite_storage->getQuery()
+        ->allRevisions()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+    }
+
+    if (!isset($context['results'][$entity_type_id])) {
+      $context['results'][$entity_type_id]['entity_count'] = 0;
+      $context['results'][$entity_type_id]['revision_count'] = 0;
+      $context['results'][$entity_type_id]['start'] = $this->time->getRequestTime();
+    }
+
+    // Get the next batch of revision ids from the selected entity type.
+    // @todo Replace with an entity query on all revisions with a revision ID
+    //   condition after https://www.drupal.org/project/drupal/issues/2766135.
+    $revision_table = $composite_type->getRevisionTable();
+    $entity_revision_ids = $this->database->select($revision_table, 'r')
+      ->fields('r', [$composite_revision_key])
+      ->range(0, $batch_size)
+      ->orderBy($composite_revision_key)
+      ->condition($composite_revision_key, $context['sandbox']['current_revision_id'], '>')
+      ->execute()
+      ->fetchCol();
+
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */
+    foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) {
+      $context['sandbox']['progress']++;
+      $context['sandbox']['current_revision_id'] = $composite_revision->getRevisionId();
+
+      if ($this->isUsed($composite_revision)) {
+        continue;
+      }
+
+      if ($this->deleteUnusedRevision($composite_revision)) {
+        $context['results'][$entity_type_id]['revision_count']++;
+        if ($composite_revision->isDefaultRevision()) {
+          $context['results'][$entity_type_id]['entity_count']++;
+        }
+      }
+    }
+
+    // This entity type is completed if no new revision ids were found or the
+    // total is reached.
+    if ($entity_revision_ids && $context['sandbox']['progress'] < $context['sandbox']['total']) {
+      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total'];
+    }
+    else {
+      $context['finished'] = 1;
+      $context['results'][$entity_type_id]['end'] = $this->time->getRequestTime();
+    }
+
+    $interval = $this->dateFormatter->formatInterval($this->time->getRequestTime() - $context['results'][$entity_type_id]['start']);
+    $context['message'] = t('Checked @entity_type revisions for orphans: @current of @total in @interval (@deletions deleted)', [
+      '@entity_type' => $composite_type->getLabel(),
+      '@current' => $context['sandbox']['progress'],
+      '@total' => $context['sandbox']['total'],
+      '@interval' => $interval,
+      '@deletions' => $context['results'][$entity_type_id]['revision_count'],
+    ]);
+  }
+
+  /**
+   * Batch dispatch submission finished callback.
+   */
+  public static function batchSubmitFinished($success, $results, $operations) {
+    return \Drupal::service('entity_reference_revisions.orphan_purger')->doBatchSubmitFinished($success, $results, $operations);
+  }
+
+  /**
+   * Sets a batch for executing deletion of the orphaned composite entities.
+   *
+   * @param array $composite_entity_type_ids
+   *   An array of composite entity type IDs to remove orphaned items for.
+   */
+  public function setBatch(array $composite_entity_type_ids) {
+    if (empty($composite_entity_type_ids)) {
+      return;
+    }
+
+    $operations = [];
+    foreach ($composite_entity_type_ids as $entity_type_id) {
+      $operations[] = ['_entity_reference_revisions_orphan_purger_batch_dispatcher',
+        [
+          'entity_reference_revisions.orphan_purger:deleteOrphansBatchOperation',
+          $entity_type_id,
+        ],
+      ];
+    }
+
+    $batch = [
+      'operations' => $operations,
+      'finished' => [EntityReferenceRevisionsOrphanPurger::class, 'batchSubmitFinished'],
+      'title' => $this->t('Removing orphaned entities.'),
+      'progress_message' => $this->t('Processed @current of @total entity types.'),
+      'error_message' => $this->t('This batch encountered an error.'),
+    ];
+    batch_set($batch);
+  }
+
+  /**
+   * Finished callback for the batch process.
+   *
+   * @param bool $success
+   *   Whether the batch completed successfully.
+   * @param array $results
+   *   The results array.
+   * @param array $operations
+   *   The operations array.
+   */
+  public function doBatchSubmitFinished($success, $results, $operations) {
+    if ($success) {
+      foreach ($results as $entity_type_id => $result) {
+        if ($this->entityTypeManager->hasDefinition($entity_type_id)) {
+          $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+          $interval = $this->dateFormatter->formatInterval($result['end'] - $result['start']);
+          $this->messenger->addMessage($this->t('@label: Deleted @revision_count revisions (@entity_count entities) in @interval.', [
+            '@label' => $entity_type->getLabel(),
+            '@revision_count' => $result['revision_count'],
+            '@entity_count' => $result['entity_count'],
+            '@interval' => $interval,
+          ]));
+        }
+      }
+    }
+    else {
+      // $operations contains the operations that remained unprocessed.
+      $error_operation = reset($operations);
+      $this->messenger->addError($this->t('An error occurred while processing @operation with arguments : @args', [
+        '@operation' => $error_operation[0],
+        '@args' => print_r($error_operation[0], TRUE),
+      ]));
+    }
+  }
+
+  /**
+   * Checks if the composite entity is used.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision
+   *   The composite revision.
+   *
+   * @return bool
+   *   Whether the composite entity is used, FALSE if it is safe to delete.
+   */
+  public function isUsed(ContentEntityInterface $composite_revision) {
+    $composite_type = $this->entityTypeManager->getDefinition($composite_revision->getEntityTypeId());
+
+    $parent_type_field = $composite_type->get('entity_revision_parent_type_field');
+    $parent_type = $composite_revision->get($parent_type_field)->value;
+    $parent_field_name_field = $composite_type->get('entity_revision_parent_field_name_field');
+    $parent_field_name = $composite_revision->get($parent_field_name_field)->value;
+
+    $status = $this->isValidParent($parent_type, $parent_field_name);
+    if ($status !== static::PARENT_VALID) {
+      return $status == static::PARENT_INVALID_SKIP ? TRUE : FALSE;
+    }
+
+    // Check if the revision is used in any revision of the parent, if that
+    // entity type supports revisions.
+    $query = $this->entityTypeManager->getStorage($parent_type)
+      ->getQuery()
+      ->condition("$parent_field_name.target_revision_id", $composite_revision->getRevisionId())
+      ->range(0, 1)
+      ->accessCheck(FALSE);
+
+    if ($this->entityTypeManager->getDefinition($parent_type)->isRevisionable()) {
+      $query = $query->allRevisions();
+    }
+
+    $revisions = $query->execute();
+    // If there are parent revisions where this revision is used, skip it.
+    return !empty($revisions);
+  }
+
+  /**
+   * Checks if the parent type/field is a valid combination that can be queried.
+   *
+   * @param string $parent_type
+   *   Parent entity type ID.
+   * @param string $parent_field_name
+   *   Parent field name.
+   *
+   * @return int
+   *   static::PARENT_VALID, static::PARENT_INVALID_SKIP or
+   *   static::PARENT_INVALID_DELETE.
+   */
+  protected function isValidParent($parent_type, $parent_field_name) {
+    // There is not certainty that this revision is not used because we do not
+    // know what to query for if the parent fields are empty.
+    if ($parent_type == NULL) {
+      return static::PARENT_INVALID_SKIP;
+    }
+
+    if (isset($this->validParents[$parent_type][$parent_field_name])) {
+      return $this->validParents[$parent_type][$parent_field_name];
+    }
+
+    $status = static::PARENT_VALID;
+    // If the parent type does not exist anymore, the composite is not used.
+    if (!$this->entityTypeManager->hasDefinition($parent_type)) {
+      $status = static::PARENT_INVALID_DELETE;
+    }
+    // Check if the parent field is valid.
+    elseif (!($parent_field_config = $this->entityTypeManager->getStorage('field_storage_config')->load("$parent_type.$parent_field_name"))) {
+      $status = static::PARENT_INVALID_DELETE;
+    }
+    // In case the parent field has no target revision ID key we can not be sure
+    // that this revision is not used anymore.
+    elseif (empty($parent_field_config->getSchema()['columns']['target_revision_id'])) {
+      $status = static::PARENT_INVALID_SKIP;
+    }
+    $this->validParents[$parent_type][$parent_field_name] = $status;
+
+    return $status;
+  }
+
+}
diff --git a/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php b/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..b3da36da6bfd8fdadf272c8543b3eba134f0bdee
--- /dev/null
+++ b/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Form;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Class OrphanedCompositeEntitiesDeleteForm.
+ *
+ * @package Drupal\entity_reference_revisions\Form
+ */
+class OrphanedCompositeEntitiesDeleteForm extends FormBase {
+
+  /**
+   * The Entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * The entity reference revisions orphan purger service.
+   *
+   * @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger
+   */
+  protected $purger;
+
+  /**
+   * OrphanedCompositeEntitiesDeleteForm constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
+   *   The EntityTypeManager service.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
+   * @param \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger $purger
+   *   The entity reference revisions orphan purger.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_manager, MessengerInterface $messenger, EntityReferenceRevisionsOrphanPurger $purger) {
+    $this->entityTypeManager = $entity_manager;
+    $this->messenger = $messenger;
+    $this->purger = $purger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('messenger'),
+      $container->get('entity_reference_revisions.orphan_purger')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'orphaned_composite_entities_delete_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $this->messenger->addWarning($this->t('The submission of the current form can cause the deletion of entities that are still used, backup all data first.'), 'warning');
+    $form['description'] = [
+      '#markup' => $this->t('Delete orphaned composite entities revisions that are no longer referenced. If there are no revisions left, the entity will be deleted as long as it is not used.'),
+    ];
+    $options = [];
+    foreach ($this->getCompositeEntityTypes() as $entity_type) {
+      $options[$entity_type->id()] = $entity_type->getLabel();
+    }
+    $form['composite_entity_types'] = [
+      '#type' => 'checkboxes',
+      '#required' => TRUE,
+      '#title' => $this->t('Select the entity types to check for orphans'),
+      '#options' => $options,
+      '#default_value' => array_keys($options),
+    ];
+    $form['actions'] = ['#type' => 'actions'];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Delete orphaned composite revisions'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->purger->setBatch(array_filter($form_state->getValue('composite_entity_types')));
+  }
+
+  /**
+   * Returns a list of composite entity types.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface[]
+   *   An array of composite entity types.
+   */
+  public function getCompositeEntityTypes() {
+    $composite_entity_types = [];
+    $entity_types = $this->entityTypeManager->getDefinitions();
+    foreach ($entity_types as $entity_type) {
+      $has_parent_type_field = $entity_type->get('entity_revision_parent_type_field');
+      $has_parent_id_field = $entity_type->get('entity_revision_parent_id_field');
+      $has_parent_field_name_field = $entity_type->get('entity_revision_parent_field_name_field');
+      if ($has_parent_type_field && $has_parent_id_field && $has_parent_field_name_field) {
+        $composite_entity_types[] = $entity_type;
+      }
+    }
+    return $composite_entity_types;
+  }
+
+}
diff --git a/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php
index c9e66ac6656263e0517264bd43baff8285cd3528..b24b6b10765f89cfe2c6ac420dd37f82f46721db 100644
--- a/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php
+++ b/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php
@@ -73,8 +73,13 @@ public function isTargetNew() {
    */
   public function getTarget() {
     if (!isset($this->target) && isset($this->revision_id)) {
-      // If we have a valid reference, return the entity's TypedData adapter.
-      $entity = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId())->loadRevision($this->revision_id);
+      $storage = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId());
+      // By default always load the default revision, so caches get used.
+      $entity = $storage->load($this->id);
+      if ($entity !== NULL && $entity->getRevisionId() != $this->revision_id) {
+        // A non-default revision is a referenced, so load this one.
+        $entity = $storage->loadRevision($this->revision_id);
+      }
       $this->target = isset($entity) ? $entity->getTypedData() : NULL;
     }
     return $this->target;
diff --git a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php
index cdf2867b1b389046f0c2ea54a20fdfadda0b875e..16496dbc7e47247060fba0fa32077359b520d337 100644
--- a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php
+++ b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType;
 
+use Drupal\Component\Utility\Random;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\TranslatableRevisionableInterface;
 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
@@ -179,7 +181,7 @@ public function setValue($values, $notify = TRUE) {
         // If the entity has been saved and we're trying to set both the
         // target_id and the entity values with a non-null target ID, then the
         // value for target_id should match the ID of the entity value.
-        if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id !== $values['target_id'])) {
+        if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) {
           throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.');
         }
       }
@@ -253,24 +255,55 @@ public function preSave() {
     // If it is a new entity, parent will save it.
     parent::preSave();
 
+    $is_affected = TRUE;
     if (!$has_new) {
       // Create a new revision if it is a composite entity in a host with a new
       // revision.
 
       $host = $this->getEntity();
       $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave();
-      if (!$host->isNew() && $host->isNewRevision() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
-        $this->entity->setNewRevision();
-        if ($host->isDefaultRevision()) {
-          $this->entity->isDefaultRevision(TRUE);
+
+      // The item is considered to be affected if the field is either
+      // untranslatable or there are translation changes. This ensures that for
+      // translatable fields, a new revision of the referenced entity is only
+      // created for the affected translations and that the revision ID does not
+      // change on the unaffected translations. In turn, the host entity is not
+      // marked as affected for these translations.
+      $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges());
+      if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
+        if ($host->isNewRevision()) {
+          $this->entity->setNewRevision();
+          $needs_save = TRUE;
+        }
+        // Additionally ensure that the default revision state is kept in sync.
+        if ($this->entity && $host->isDefaultRevision() != $this->entity->isDefaultRevision()) {
+          $this->entity->isDefaultRevision($host->isDefaultRevision());
+          $needs_save = TRUE;
         }
-        $needs_save = TRUE;
       }
       if ($needs_save) {
+
+        // Because ContentEntityBase::hasTranslationChanges() does not check for
+        // EntityReferenceRevisionsFieldItemList::hasAffectingChanges() on field
+        // items that are not translatable, hidden on translation forms and not
+        // in the default translation, this has to be handled here by setting
+        // setRevisionTranslationAffected on host translations that holds a
+        // reference that has been changed.
+        if ($is_affected && $host instanceof TranslatableRevisionableInterface) {
+          $languages = $host->getTranslationLanguages();
+          foreach ($languages as $langcode => $language) {
+            $translation = $host->getTranslation($langcode);
+            if ($this->entity->hasTranslation($langcode) && $this->entity->getTranslation($langcode)->hasTranslationChanges() && $this->target_revision_id != $this->entity->getRevisionId()) {
+              $translation->setRevisionTranslationAffected(TRUE);
+              $translation->setRevisionTranslationAffectedEnforced(TRUE);
+            }
+          }
+        }
+
         $this->entity->save();
       }
     }
-    if ($this->entity) {
+    if ($this->entity && $is_affected) {
       $this->target_revision_id = $this->entity->getRevisionId();
     }
   }
@@ -301,6 +334,22 @@ public function postSave($update) {
       }
     }
 
+    // Keep in sync the translation languages between the parent and the child.
+    // For non translatable fields we have to do this in ::preSave but for
+    // translatable fields we have all the information we need in ::delete.
+    if (isset($parent_entity->original) && !$this->getFieldDefinition()->isTranslatable()) {
+      $langcodes = array_keys($parent_entity->getTranslationLanguages());
+      $original_langcodes = array_keys($parent_entity->original->getTranslationLanguages());
+      if ($removed_langcodes = array_diff($original_langcodes, $langcodes)) {
+        foreach ($removed_langcodes as $removed_langcode) {
+          if ($entity->hasTranslation($removed_langcode)  && $entity->getUntranslated()->language()->getId() != $removed_langcode) {
+            $entity->removeTranslation($removed_langcode);
+          }
+        }
+        $needs_save = TRUE;
+      }
+    }
+
     $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field');
     $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field');
 
@@ -328,8 +377,11 @@ public function postSave($update) {
    */
   public function deleteRevision() {
     $child = $this->entity;
-    if ($child->isDefaultRevision()) {
-      // Do not delete if it is the default revision.
+    // Return early, and do not delete the child revision, when the child
+    // revision is either:
+    // 1: Missing.
+    // 2: A default revision.
+    if (!$child || $child->isDefaultRevision()) {
       return;
     }
 
@@ -338,6 +390,7 @@ public function deleteRevision() {
     $all_revisions = \Drupal::entityQuery($host->getEntityTypeId())
       ->condition($field_name, $child->getRevisionId())
       ->allRevisions()
+      ->accessCheck(FALSE)
       ->execute();
 
     if (count($all_revisions) > 1) {
@@ -357,15 +410,59 @@ public function delete() {
     if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
       // Only delete composite entities if the host field is not translatable.
       if (!$this->getFieldDefinition()->isTranslatable()) {
-        $this->entity->delete();
+        \Drupal::queue('entity_reference_revisions_orphan_purger')->createItem([
+          'entity_id' => $this->entity->id(),
+          'entity_type_id' => $this->entity->getEntityTypeId(),
+        ]);
       }
     }
   }
- /**
- * {@inheritdoc}
- */
+
+  /**
+   * {@inheritdoc}
+   */
   public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) {
-    return FALSE;
+    $changed = FALSE;
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $target_entity_type = $entity_type_manager->getDefinition($field_definition->getFieldStorageDefinition()
+      ->getSetting('target_type'));
+    $handler_settings = $field_definition->getSetting('handler_settings');
+
+    // Update the 'target_bundles' handler setting if a bundle config dependency
+    // has been removed.
+    if (!empty($handler_settings['target_bundles'])) {
+      if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) {
+        if ($storage = $entity_type_manager->getStorage($bundle_entity_type_id)) {
+          foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) {
+            if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) {
+              unset($handler_settings['target_bundles'][$bundle->id()]);
+              $changed = TRUE;
+
+              // In case we deleted the only target bundle allowed by the field
+              // we can log a message because the behaviour of the field will
+              // have changed.
+              if ($handler_settings['target_bundles'] === []) {
+                \Drupal::logger('entity_reference_revisions')
+                  ->notice('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference revisions field (entity_type: %entity_type, bundle: %bundle) no longer specifies a specific target bundle. The field will now accept any bundle and may need to be adjusted.', [
+                    '%target_bundle' => $bundle->label(),
+                    '%target_entity_type' => $bundle->getEntityType()
+                      ->getBundleOf(),
+                    '%field_name' => $field_definition->getName(),
+                    '%entity_type' => $field_definition->getTargetEntityTypeId(),
+                    '%bundle' => $field_definition->getTargetBundle()
+                  ]);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if ($changed) {
+      $field_definition->setSetting('handler_settings', $handler_settings);
+    }
+
+    return $changed;
   }
 
   /**
diff --git a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php
index faad139aba5389e3aa58ecd1e9559ce655d7f81c..c1d5ee51d96b4e2c4ad80f1c4afbec8d80dc8d04 100644
--- a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php
+++ b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\entity_reference_revisions\Plugin\Field\FieldWidget;
 
-use Drupal\Core\Entity\Entity;
 use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget;
 use Drupal\Core\Form\FormStateInterface;
 
diff --git a/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php b/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php
new file mode 100644
index 0000000000000000000000000000000000000000..a2bf051f1badda6050c5aade5d5a461fb90a8c57
--- /dev/null
+++ b/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\QueueWorker;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Queue\QueueWorkerBase;
+use Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Removes composite revisions that are no longer used.
+ *
+ * @QueueWorker(
+ *   id = "entity_reference_revisions_orphan_purger",
+ *   title = @Translation("Entity Reference Revisions Orphan Purger"),
+ *   cron = {"time" = 60}
+ * )
+ */
+class OrphanPurger extends QueueWorkerBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The purger.
+   *
+   * @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger
+   */
+  protected $purger;
+
+  /**
+   * The database.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Constructs a new OrphanPurger instance.
+   *
+   * @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\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger $purger
+   *   The purger service.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityReferenceRevisionsOrphanPurger $purger, Connection $database) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->purger = $purger;
+    $this->database = $database;
+  }
+
+  /**
+   * {@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('entity_reference_revisions.orphan_purger'),
+      $container->get('database')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processItem($data) {
+    $entity_type_id = $data['entity_type_id'];
+    if (!$this->entityTypeManager->hasDefinition($entity_type_id)) {
+      return;
+    }
+
+    // Check the usage of data item and remove if not used.
+    $composite_storage = $this->entityTypeManager->getStorage($entity_type_id);
+    $composite_type = $this->entityTypeManager->getDefinition($entity_type_id);
+    $composite_revision_key = $composite_type->getKey('revision');
+
+    // Load all revisions of the composite type.
+    // @todo Replace with an entity query on all revisions with a revision ID
+    //   condition after https://www.drupal.org/project/drupal/issues/2766135.
+    $entity_revision_ids = $this->database->select($composite_type->getRevisionTable(), 'r')
+      ->fields('r', [$composite_revision_key])
+      ->condition($composite_type->getKey('id'), $data['entity_id'])
+      ->orderBy($composite_revision_key)
+      ->execute()
+      ->fetchCol();
+
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */
+    foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) {
+      if (!$this->purger->isUsed($composite_revision)) {
+        $this->purger->deleteUnusedRevision($composite_revision);
+      }
+    }
+  }
+
+}
diff --git a/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php
index a50f5a76750cf719b1e5473a56b0eb7730ae65cb..d4baf31c65e7076e773e56993ba73ec2a8c0523e 100644
--- a/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php
+++ b/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\entity_reference_revisions\Plugin\migrate\destination;
 
+use Drupal\Component\Plugin\ConfigurableInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\migrate\MigrateException;
@@ -12,12 +13,40 @@
 /**
  * Provides entity_reference_revisions destination plugin.
  *
+ * Available configuration keys:
+ * - new_revisions: (optional) Flag to indicate if a new revision should be
+ *   created instead of updating a previous default record. Only applicable when
+ *   providing an entity id without a revision_id.
+ *
  * @MigrateDestination(
  *   id = "entity_reference_revisions",
  *   deriver = "Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions"
  * )
  */
-class EntityReferenceRevisions extends EntityRevision {
+class EntityReferenceRevisions extends EntityRevision implements ConfigurableInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'new_revisions' => FALSE,
+    ];
+  }
 
   /**
    * {@inheritdoc}
@@ -32,7 +61,7 @@ protected static function getEntityTypeId($pluginId) {
   /**
    * {@inheritdoc}
    */
-  protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = array()) {
+  protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = []) {
     $entity->save();
 
     return [
@@ -70,12 +99,33 @@ public function getIds() {
    * {@inheritdoc}
    */
   protected function getEntity(Row $row, array $oldDestinationIdValues) {
+    $entity_id = $oldDestinationIdValues ?
+      array_shift($oldDestinationIdValues) :
+      $this->getEntityId($row);
     $revision_id = $oldDestinationIdValues ?
       array_pop($oldDestinationIdValues) :
       $row->getDestinationProperty($this->getKey('revision'));
+
+    // If a specific revision_id is supplied and exists, assert the entity_id
+    // matches (if supplied), and update the revision.
+    /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityInterface $entity */
     if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) {
-      $entity->setNewRevision(FALSE);
+      if (!empty($entity_id) && ($entity->id() != $entity_id)) {
+        throw new MigrateException("The revision_id exists for this entity type, but does not belong to the given entity id");
+      }
+      $entity = $this->updateEntity($entity, $row) ?: $entity;
+    }
+    // If there is no revision_id supplied, but there is an entity_id
+    // supplied that exists, update it.
+    elseif (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
+      // If so configured, create a new revision while updating.
+      if (!empty($this->configuration['new_revisions'])) {
+        $entity->setNewRevision(TRUE);
+      }
+      $entity = $this->updateEntity($entity, $row) ?: $entity;
     }
+
+    // Otherwise, create a new (possibly stub) entity.
     else {
       // Attempt to ensure we always have a bundle.
       if ($bundle = $this->getBundle($row)) {
@@ -90,7 +140,6 @@ protected function getEntity(Row $row, array $oldDestinationIdValues) {
         ->enforceIsNew(TRUE);
       $entity->setNewRevision(TRUE);
     }
-    $entity = $this->updateEntity($entity, $row) ?: $entity;
     $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
     return $entity;
   }
@@ -117,8 +166,8 @@ protected function rollbackTranslation(array $destination_identifiers) {
     $entity = $this->storage->loadRevision(array_pop($destination_identifiers));
     if ($entity && $entity instanceof TranslatableInterface) {
       if ($key = $this->getKey('langcode')) {
-        if (isset($destination_identifier[$key])) {
-          $langcode = $destination_identifier[$key];
+        if (isset($destination_identifiers[$key])) {
+          $langcode = $destination_identifiers[$key];
           if ($entity->hasTranslation($langcode)) {
             // Make sure we don't remove the default translation.
             $translation = $entity->getTranslation($langcode);
@@ -150,4 +199,5 @@ protected function rollbackNonTranslation(array $destination_identifiers) {
       }
     }
   }
+
 }
diff --git a/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php
index 3c738564d2f4174dea61020c5770f686a7333565..65ad121f9bb692ccc0e6df8529a3857d787446a2 100644
--- a/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php
+++ b/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\entity_reference_revisions\Plugin\views\display;
 
+use Drupal\Core\Database\Query\Condition;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 
 /**
@@ -123,13 +124,13 @@ public function query() {
     // Restrict the autocomplete options based on what's been typed already.
     if (isset($options['match'])) {
       $style_options = $this->getOption('style');
-      $value = db_like($options['match']) . '%';
+      $value = \Drupal::database()->escapeLike($options['match']) . '%';
       if ($options['match_operator'] != 'STARTS_WITH') {
         $value = '%' . $value;
       }
 
       // Multiple search fields are OR'd together.
-      $conditions = db_or();
+      $conditions = new Condition('OR');
 
       // Build the condition using the selected search fields.
       foreach ($style_options['options']['search_fields'] as $field_alias) {
diff --git a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml
index d81f3c3672c4ee1be2daaed0b8b46c91c5f1a6d0..7b906fc29d371869a923883fddcde58671fc1faf 100644
--- a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml
+++ b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml
@@ -2,13 +2,13 @@ name: 'ERR Composite relationship test'
 type: module
 description: 'Entity with parent type and ID.'
 package: Testing
-# core: 8.x
+core_version_requirement: ^8.7.7 || ^9
 
 dependencies:
- - entity_reference_revisions
- - entity_test
-# Information added by Drupal.org packaging script on 2017-05-26
-version: '8.x-1.3'
-core: '8.x'
+ - entity_reference_revisions:entity_reference_revisions
+ - drupal:entity_test
+
+# Information added by Drupal.org packaging script on 2020-03-11
+version: '8.x-1.8'
 project: 'entity_reference_revisions'
-datestamp: 1495814304
+datestamp: 1583961849
diff --git a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php
index fb61ad15940d4ac80a6458a7b10a51b5be932f1e..9ef825c409854c2d8bedfcffd347e8c303d080d1 100644
--- a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php
+++ b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php
@@ -18,6 +18,7 @@
  *   revision_table = "entity_test_composite_revision",
  *   data_table = "entity_test_composite_field_data",
  *   revision_data_table = "entity_test_composite_field_revision",
+ *   content_translation_ui_skip = TRUE,
  *   translatable = TRUE,
  *   entity_revision_parent_type_field = "parent_type",
  *   entity_revision_parent_id_field = "parent_id",
@@ -44,15 +45,18 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     $fields = parent::baseFieldDefinitions($entity_type);
     $fields['parent_id'] = BaseFieldDefinition::create('string')
       ->setLabel(t('Parent ID'))
-      ->setDescription(t('The ID of the parent entity of which this entity is referenced.'));
+      ->setDescription(t('The ID of the parent entity of which this entity is referenced.'))
+      ->setRevisionable(TRUE);
 
     $fields['parent_type'] = BaseFieldDefinition::create('string')
       ->setLabel(t('Parent type'))
-      ->setDescription(t('The entity parent type to which this entity is referenced.'));
+      ->setDescription(t('The entity parent type to which this entity is referenced.'))
+      ->setRevisionable(TRUE);
 
     $fields['parent_field_name'] = BaseFieldDefinition::create('string')
       ->setLabel(t('Parent field name'))
-      ->setDescription(t('The entity parent field name to which this entity is referenced.'));
+      ->setDescription(t('The entity parent field name to which this entity is referenced.'))
+      ->setRevisionable(TRUE);
 
     return $fields;
   }
diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php
similarity index 74%
rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php
rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php
index e0a4c592740a2ce3871045364112aedc8b072deb..f2618b914f8337a868ec65d627c1d37a85e734a3 100644
--- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php
@@ -1,17 +1,17 @@
 <?php
 
-namespace Drupal\entity_reference_revisions\Tests;
+namespace Drupal\Tests\entity_reference_revisions\Functional;
 
-use Drupal\field_ui\Tests\FieldUiTestTrait;
 use Drupal\node\Entity\Node;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 
 /**
  * Tests the entity_reference_revisions configuration.
  *
  * @group entity_reference_revisions
  */
-class EntityReferenceRevisionsAdminTest extends WebTestBase {
+class EntityReferenceRevisionsAdminTest extends BrowserTestBase {
 
   use FieldUiTestTrait;
 
@@ -28,6 +28,11 @@ class EntityReferenceRevisionsAdminTest extends WebTestBase {
     'block',
   );
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * {@inheritdoc}
    */
@@ -38,12 +43,6 @@ protected function setUp() {
     $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
     // Place the breadcrumb, tested in fieldUIAddNewField().
     $this->drupalPlaceBlock('system_breadcrumb_block');
-  }
-
-  /**
-   * Tests the entity reference revisions configuration.
-   */
-  public function testEntityReferenceRevisions() {
     $admin_user = $this->drupalCreateUser(array(
       'administer site configuration',
       'administer nodes',
@@ -55,7 +54,12 @@ public function testEntityReferenceRevisions() {
       'edit any article content',
     ));
     $this->drupalLogin($admin_user);
+  }
 
+  /**
+   * Tests the entity reference revisions configuration.
+   */
+  public function testEntityReferenceRevisions() {
     // Create a test target node used as entity reference by another test node.
     $node_target = Node::create([
       'title' => 'Target node',
@@ -90,7 +94,7 @@ public function testEntityReferenceRevisions() {
       'title[0][value]' => $title,
       'body[0][value]' => 'Revision 1',
     );
-    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/article', $edit, t('Save'));
     $this->assertText($title);
     $this->assertText('Revision 1');
     $node = $this->drupalGetNodeByTitle($title);
@@ -103,7 +107,7 @@ public function testEntityReferenceRevisions() {
       'title[0][value]' => 'Entity reference revision content',
       'field_entity_reference_revisions[1][target_id]' => $node->label() . ' (' . $node->id() . ')',
     ];
-    $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+    $this->drupalPostForm(NULL, $edit, t('Save'));
     $this->assertLinkByHref('node/' . $node_target->id());
     $this->assertText('Entity revisions Entity reference revision content has been created.');
     $this->assertText('Entity reference revision content');
@@ -115,7 +119,7 @@ public function testEntityReferenceRevisions() {
       'body[0][value]' => 'Revision 2',
       'revision' => TRUE,
     );
-    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
     $this->assertText($title);
     $this->assertText('Revision 2');
 
@@ -148,4 +152,37 @@ public function testEntityReferenceRevisions() {
     $this->assertEqual((string) $properties['entity']->getLabel(), 'Content');
   }
 
+  /**
+   * Tests target bundle settings for an entity reference revisions field.
+   */
+  public function testMultipleTargetBundles() {
+    // Create a couple of content types for the ERR field to point to.
+    $target_types = [];
+    for ($i = 0; $i < 2; $i++) {
+      $target_types[$i] = $this->drupalCreateContentType([
+        'type' => strtolower($this->randomMachineName()),
+        'name' => 'Test type ' . $i
+      ]);
+    }
+
+    // Create a new field that can point to either target content type.
+    $node_type_path = 'admin/structure/types/manage/entity_revisions';
+
+    // Generate a random field name, must be only lowercase characters.
+    $field_name = strtolower($this->randomMachineName());
+
+    $field_edit = [];
+    $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1'];
+    $field_edit['settings[handler_settings][target_bundles][' . $target_types[0]->id() . ']'] = TRUE;
+    $field_edit['settings[handler_settings][target_bundles][' . $target_types[1]->id() . ']'] = TRUE;
+
+    $this->fieldUIAddNewField($node_type_path, $field_name, 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit);
+
+    // Deleting one of these content bundles at this point should only delete
+    // that bundle's body field. Test that there is no second field that will
+    // be deleted.
+    $this->drupalGet('/admin/structure/types/manage/' . $target_types[0]->id() . '/delete');
+    $this->assertNoFieldByXPath('(//details[@id="edit-entity-deletes"]//ul[@data-drupal-selector="edit-field-config"]/li)[2]');
+  }
+
 }
diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php
similarity index 92%
rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php
rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php
index 4998a41cee51c9164182098c824f4068bd422b88..95cff09154cc2b9a6778faa3ea025dc94999182e 100644
--- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php
@@ -1,19 +1,19 @@
 <?php
 
-namespace Drupal\entity_reference_revisions\Tests;
+namespace Drupal\Tests\entity_reference_revisions\Functional;
 
 use Drupal\block_content\Entity\BlockContent;
 use Drupal\Component\Utility\Html;
-use Drupal\field_ui\Tests\FieldUiTestTrait;
 use Drupal\node\Entity\Node;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 
 /**
  * Tests the entity_reference_revisions autocomplete.
  *
  * @group entity_reference_revisions
  */
-class EntityReferenceRevisionsAutocompleteTest extends WebTestBase {
+class EntityReferenceRevisionsAutocompleteTest extends BrowserTestBase {
 
   use FieldUiTestTrait;
 
@@ -30,6 +30,11 @@ class EntityReferenceRevisionsAutocompleteTest extends WebTestBase {
     'field_ui',
   );
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * {@inheritdoc}
    */
@@ -91,7 +96,7 @@ public function testEntityReferenceRevisionsAutocompleteProcessing() {
       'body[0][value]' => 'Revision 1',
       'field_entity_reference_revisions[0][target_id]' => $block_label . ' (' . $block->id() . ')',
     );
-    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/article', $edit, t('Save'));
     $this->assertText($title);
     $this->assertText(Html::escape($block_content));
 
diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php
similarity index 83%
rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php
rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php
index e8ea4ad0c66cc56770ee8a3e9e048903d16f508d..a79d4d94dbbb8bf5029faa00458b05d10f46a2b5 100644
--- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php
@@ -1,9 +1,9 @@
 <?php
 
-namespace Drupal\entity_reference_revisions\Tests;
+namespace Drupal\Tests\entity_reference_revisions\Functional;
 
-use Drupal\field_ui\Tests\FieldUiTestTrait;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 
 /**
  * Tests the entity_reference_revisions diff plugin.
@@ -12,9 +12,10 @@
  *
  * @dependencies diff
  */
-class EntityReferenceRevisionsDiffTest extends WebTestBase {
+class EntityReferenceRevisionsDiffTest extends BrowserTestBase {
 
   use FieldUiTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -29,6 +30,11 @@ class EntityReferenceRevisionsDiffTest extends WebTestBase {
     'diff',
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * {@inheritdoc}
    */
@@ -76,7 +82,7 @@ public function testEntityReferenceRevisionsDiff() {
       'title[0][value]' => $title_node_1,
       'body[0][value]' => 'body_node_1',
     ];
-    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/article', $edit, t('Save'));
 
     // Create second referenced node.
     $title_node_2 = 'referenced_node_2';
@@ -84,7 +90,7 @@ public function testEntityReferenceRevisionsDiff() {
       'title[0][value]' => $title_node_2,
       'body[0][value]' => 'body_node_2',
     ];
-    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/article', $edit, t('Save'));
 
     // Create referencing node.
     $title = 'referencing_node';
@@ -93,11 +99,11 @@ public function testEntityReferenceRevisionsDiff() {
       'title[0][value]' => $title,
       'field_err_field[0][target_id]' => $title_node_1 . ' (' . $node->id() . ')',
     ];
-    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/article', $edit, t('Save'));
 
     // Check the plugin is set.
     $this->drupalGet('admin/config/content/diff/fields');
-    $this->drupalPostForm(NULL, ['fields[node.field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save'));
+    $this->drupalPostForm(NULL, ['fields[node__field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save'));
 
     // Update the referenced node of the err field and create a new revision.
     $node = $this->drupalGetNodeByTitle($title);
@@ -106,7 +112,7 @@ public function testEntityReferenceRevisionsDiff() {
       'field_err_field[0][target_id]' => $title_node_2 . ' (' . $referenced_node_new->id() . ')',
       'revision' => TRUE,
     ];
-    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
 
     // Compare the revisions of the referencing node.
     $this->drupalPostForm('node/' . $node->id() . '/revisions', [], t('Compare selected revisions'));
diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php
similarity index 90%
rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php
rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php
index 3faf8fb41a044234014364b2f41e2cd28f88b375..740fe8b1927ca8a430b70b0acb8a6a163cd211fb 100644
--- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php
@@ -1,17 +1,17 @@
 <?php
 
-namespace Drupal\entity_reference_revisions\Tests;
+namespace Drupal\Tests\entity_reference_revisions\Functional;
 
-use Drupal\field_ui\Tests\FieldUiTestTrait;
 use Drupal\node\Entity\Node;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
 
 /**
  * Tests the entity_reference_revisions configuration.
  *
  * @group entity_reference_revisions
  */
-class EntityReferenceRevisionsNormalizerTest extends WebTestBase {
+class EntityReferenceRevisionsNormalizerTest extends BrowserTestBase {
 
   use FieldUiTestTrait;
 
@@ -31,6 +31,11 @@ class EntityReferenceRevisionsNormalizerTest extends WebTestBase {
     'rest',
   );
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * {@inheritdoc}
    */
@@ -68,7 +73,7 @@ public function testEntityReferenceRevisions() {
       'title[0][value]' => $title,
       'body[0][value]' => 'Revision 1',
     );
-    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/article', $edit, t('Save'));
     $this->assertText($title);
     $this->assertText('Revision 1');
     $node = $this->drupalGetNodeByTitle($title);
@@ -79,7 +84,7 @@ public function testEntityReferenceRevisions() {
       'title[0][value]' => $err_title,
       'field_entity_reference_revisions[0][target_id]' => $node->label() . ' (' . $node->id() . ')',
     );
-    $this->drupalPostForm('node/add/entity_revisions', $edit, t('Save and publish'));
+    $this->drupalPostForm('node/add/entity_revisions', $edit, t('Save'));
     $this->assertText('Entity revisions Entity reference revision content has been created.');
     $err_node = $this->drupalGetNodeByTitle($err_title);
 
@@ -92,7 +97,7 @@ public function testEntityReferenceRevisions() {
       'body[0][value]' => 'Revision 2',
       'revision' => TRUE,
     );
-    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
     $serializer = $this->container->get('serializer');
     $normalized = $serializer->normalize($err_node, 'hal_json');
     $request = \Drupal::request();
diff --git a/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3fa8798747c4e1aa92bbe1bfcfb685c0a48a0a90
--- /dev/null
+++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php
@@ -0,0 +1,371 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Functional;
+
+use Drupal\Core\Site\Settings;
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests orphan composite revisions are properly removed.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsOrphanRemovalTest extends BrowserTestBase {
+
+  /**
+   * A user with administration access.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test'
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->adminUser = $this->drupalCreateUser([
+      'delete orphan revisions',
+    ]);
+    $this->drupalLogin($this->adminUser);
+    $this->insertRevisionableData();
+    $this->insertNonRevisionableData();
+  }
+
+  /**
+   * Tests that revisions that are no longer used are properly deleted.
+   */
+  public function testNotUsedRevisionDeletion() {
+    $entity_test_composite_storage = \Drupal::entityTypeManager()->getStorage('entity_test_composite');
+
+    $composite_entity_first = $entity_test_composite_storage->loadByProperties(['name' => 'first not used, second used']);
+    $composite_entity_first = reset($composite_entity_first);
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_first->id());
+
+    $composite_entity_second = $entity_test_composite_storage->loadByProperties(['name' => 'first used, second not used']);
+    $composite_entity_second = reset($composite_entity_second);
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_second->id());
+
+    $composite_entity_third = $entity_test_composite_storage->loadByProperties(['name' => 'first not used, second not used']);
+    $composite_entity_third = reset($composite_entity_third);
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_third->id());
+
+    $composite_entity_fourth = $entity_test_composite_storage->loadByProperties(['name' => '1st filled not, 2nd filled not']);
+    $composite_entity_fourth = reset($composite_entity_fourth);
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_fourth->id());
+
+    $composite_entity_fifth = $entity_test_composite_storage->loadByProperties(['name' => '1st not, 2nd used, 3rd not, 4th']);
+    $composite_entity_fifth = reset($composite_entity_fifth);
+    $this->assertRevisionCount(4, 'entity_test_composite', $composite_entity_fifth->id());
+
+    $composite_entity_sixth = $entity_test_composite_storage->loadByProperties(['name' => 'wrong parent fields']);
+    $composite_entity_sixth = reset($composite_entity_sixth);
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_sixth->id());
+
+    // Test non revisionable parent entities.
+    $composite_entity_seventh = $entity_test_composite_storage->loadByProperties(['name' => 'NR first not used, second used']);
+    $composite_entity_seventh = reset($composite_entity_seventh);
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_seventh->id());
+
+    $composite_entity_eighth = $entity_test_composite_storage->loadByProperties(['name' => 'NR first used, second not used']);
+    $composite_entity_eighth = reset($composite_entity_eighth);
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_eighth->id());
+
+    $composite_entity_ninth = $entity_test_composite_storage->loadByProperties(['name' => 'NR 1st not, 2nd, 3rd not, 4th']);
+    $composite_entity_ninth = reset($composite_entity_ninth);
+    $this->assertRevisionCount(3, 'entity_test_composite', $composite_entity_ninth->id());
+
+    // Set the batch size to 1.
+    $settings = Settings::getInstance() ? Settings::getAll() : [];
+    $settings['entity_update_batch_size'] = 1;
+    new Settings($settings);
+
+    // Run the delete process through the form.
+    $this->runDeleteForm();
+    $this->assertSession()->pageTextContains('Test entity - composite relationship: Deleted 8 revisions (1 entities)');
+
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_first->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_second->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_third->id());
+    $this->assertRevisionCount(0, 'entity_test_composite', $composite_entity_fourth->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_fifth->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_sixth->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_seventh->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_eighth->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_ninth->id());
+  }
+
+  /**
+   * Programmatically runs the 'Delete orphaned composite entities' form.
+   */
+  public function runDeleteForm() {
+    $this->drupalGet('admin/config/system/delete-orphans');
+    $this->submitForm([], t('Delete orphaned composite revisions'));
+    $this->checkForMetaRefresh();
+  }
+
+  /**
+   * Asserts the revision count of a certain entity.
+   *
+   * @param int $expected
+   *   The expected count.
+   * @param string $entity_type_id
+   *   The entity type ID, e.g. node.
+   * @param int $entity_id
+   *   The entity ID.
+   */
+  protected function assertRevisionCount($expected, $entity_type_id, $entity_id) {
+    $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id');
+    $revision_count = \Drupal::entityQuery($entity_type_id)
+      ->condition($id_field, $entity_id)
+      ->allRevisions()
+      ->count()
+      ->execute();
+    $this->assertEquals($expected, $revision_count);
+  }
+
+  /**
+   * Inserts revisionable entities needed for testing.
+   */
+  public function insertRevisionableData() {
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+    NodeType::create(['type' => 'revisionable', 'new_revision' => TRUE])->save();
+    // Add a translatable field and a not translatable field to both content
+    // types.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_composite_entity',
+      'entity_type' => 'node',
+      'type' => 'entity_reference_revisions',
+      'settings' => [
+        'target_type' => 'entity_test_composite'
+      ],
+    ]);
+    $field_storage->save();
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'revisionable',
+      'translatable' => FALSE,
+    ]);
+    $field->save();
+
+    // Scenario 1: A composite with a default revision that is referenced and an
+    // old revision that is not. Result: Only the old revision is deleted.
+    $composite_entity_first = EntityTestCompositeRelationship::create([
+      'name' => 'first not used, second used',
+      'parent_id' => 1000,
+      'parent_type' => 'node',
+      'parent_field_name' => 'field_composite_entity',
+    ]);
+    $composite_entity_first->save();
+    $composite_entity_first = EntityTestCompositeRelationship::load($composite_entity_first->id());
+    $composite_entity_first->setNewRevision(TRUE);
+    $composite_entity_first->save();
+    $node = $this->drupalCreateNode([
+      'type' => 'revisionable',
+      'title' => 'First composite',
+      'field_composite_entity' => $composite_entity_first,
+    ]);
+    $node->save();
+
+    // Scenario 2: A composite with an old revision that is used and a default
+    // revision that is not. Result: Nothing should be deleted.
+    $composite_entity_second = EntityTestCompositeRelationship::create([
+      'name' => 'first used, second not used',
+    ]);
+    $composite_entity_second->save();
+    $node = $this->drupalCreateNode([
+      'type' => 'revisionable',
+      'title' => 'Second composite',
+      'field_composite_entity' => $composite_entity_second,
+    ]);
+    $node->save();
+    $node = $this->getNodeByTitle('Second composite');
+    $node = $node_storage->createRevision($node);
+    $node->set('field_composite_entity', NULL);
+    $node->save();
+    $composite_entity_second = EntityTestCompositeRelationship::load($composite_entity_second->id());
+    $composite_entity_second->setNewRevision(TRUE);
+    $composite_entity_second->save();
+
+    // Scenario 3: A composite with an old revision and a default revision both
+    // that are not used with empty parent fields. Result: Nothing should be
+    // deleted since we do not know if it is still used.
+    $composite_entity_third = EntityTestCompositeRelationship::create([
+      'name' => 'first not used, second not used',
+    ]);
+    $composite_entity_third->save();
+    $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id());
+    $composite_entity_third->setNewRevision(TRUE);
+    $composite_entity_third->save();
+
+    // Scenario 4: A composite with an old revision and a default revision both
+    // that are not used with filled parent fields. Result: Should first delete
+    // the old revision and then the default revision. Delete the entity too.
+    $composite_entity_fourth = EntityTestCompositeRelationship::create([
+      'name' => '1st filled not, 2nd filled not',
+      'parent_id' => 1001,
+      'parent_type' => 'node',
+      'parent_field_name' => 'field_composite_entity',
+    ]);
+    $composite_entity_fourth->save();
+    $composite_entity_fourth = EntityTestCompositeRelationship::load($composite_entity_fourth->id());
+    $composite_entity_fourth->setNewRevision(TRUE);
+    $composite_entity_fourth->set('parent_id', 1001);
+    $composite_entity_fourth->save();
+
+    // Scenario 5: A composite with many revisions and 2 at least used. Result:
+    // Delete all unused revisions.
+    $composite_entity_fifth = EntityTestCompositeRelationship::create([
+      'name' => '1st not, 2nd used, 3rd not, 4th',
+      'parent_id' => 1001,
+      'parent_type' => 'node',
+      'parent_field_name' => 'field_composite_entity',
+    ]);
+    $composite_entity_fifth->save();
+    $composite_entity_fifth = EntityTestCompositeRelationship::load($composite_entity_fifth->id());
+    $composite_entity_fifth->setNewRevision(TRUE);
+    $composite_entity_fifth->save();
+    $node = $this->drupalCreateNode([
+      'type' => 'revisionable',
+      'title' => 'Third composite',
+      'field_composite_entity' => $composite_entity_fifth,
+    ]);
+    $node->save();
+    $node = $this->getNodeByTitle('Third composite');
+    $node = $node_storage->createRevision($node);
+    $node->set('field_composite_entity', NULL);
+    $node->save();
+    $composite_entity_fifth = EntityTestCompositeRelationship::load($composite_entity_fifth->id());
+    $composite_entity_fifth->setNewRevision(TRUE);
+    $composite_entity_fifth->save();
+    $node = $this->getNodeByTitle('Third composite');
+    $node = $node_storage->createRevision($node);
+    $node->set('field_composite_entity', $composite_entity_fifth);
+    $node->save();
+
+    // Scenario 6: A composite with wrong parent fields filled pointing to a non
+    // existent parent (Parent 1). However, Parent 2 references it. Result: Must
+    // not be deleted.
+    $node = $this->drupalCreateNode([
+      'type' => 'revisionable',
+      'title' => 'DELETED composite',
+    ]);
+    $node->save();
+    $composite_entity_sixth = EntityTestCompositeRelationship::create([
+      'name' => 'wrong parent fields',
+      'parent_id' => $node->id(),
+      'parent_type' => 'node',
+      'parent_field_name' => 'field_composite_entity',
+    ]);
+    $composite_entity_sixth->save();
+    $node->delete();
+    $node = $this->drupalCreateNode([
+      'type' => 'revisionable',
+      'title' => 'Fourth composite',
+      'field_composite_entity' => $composite_entity_sixth,
+    ]);
+    $node->save();
+  }
+
+  /**
+   * Inserts non revisionable entities needed for testing.
+   */
+  public function insertNonRevisionableData() {
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    NodeType::create(['type' => 'non_revisionable', 'new_revision' => FALSE])->save();
+    // Add a translatable field and a not translatable field to both content
+    // types.
+    $field_storage = FieldStorageConfig::loadByName('node', 'field_composite_entity');
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'non_revisionable',
+      'translatable' => FALSE,
+    ]);
+    $field->save();
+
+    // Scenario 1: A composite with a default revision that is referenced and an
+    // old revision that is not. Result: Only the old revision is deleted.
+    $composite_entity_first = EntityTestCompositeRelationship::create([
+      'name' => 'NR first not used, second used',
+      'parent_id' => 1001,
+      'parent_type' => 'node',
+      'parent_field_name' => 'field_composite_entity',
+    ]);
+    $composite_entity_first->save();
+    $composite_entity_first = EntityTestCompositeRelationship::load($composite_entity_first->id());
+    $composite_entity_first->setNewRevision(TRUE);
+    $composite_entity_first->save();
+    $node = $this->drupalCreateNode([
+      'type' => 'non_revisionable',
+      'title' => 'First composite',
+      'field_composite_entity' => $composite_entity_first,
+    ]);
+    $node->save();
+
+    // Scenario 2: A composite with an old revision that is used and a default
+    // revision that is not. Result: Nothing should be deleted.
+    $composite_entity_second = EntityTestCompositeRelationship::create([
+      'name' => 'NR first used, second not used',
+    ]);
+    $composite_entity_second->save();
+    $node = $this->drupalCreateNode([
+      'type' => 'non_revisionable',
+      'title' => 'Second composite',
+      'field_composite_entity' => $composite_entity_second,
+    ]);
+    $node->save();
+    $composite_entity_second = EntityTestCompositeRelationship::load($composite_entity_second->id());
+    $composite_entity_second->setNewRevision(TRUE);
+    $composite_entity_second->save();
+
+    // Scenario 3: A composite with many revisions and 2 at least used. Result:
+    // Delete all unused revisions.
+    $composite_entity_third = EntityTestCompositeRelationship::create([
+      'name' => 'NR 1st not, 2nd, 3rd not, 4th',
+      'parent_id' => 1001,
+      'parent_type' => 'node',
+      'parent_field_name' => 'field_composite_entity',
+    ]);
+    $composite_entity_third->save();
+    $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id());
+    $composite_entity_third->setNewRevision(TRUE);
+    $composite_entity_third->save();
+    $node = $this->drupalCreateNode([
+      'type' => 'non_revisionable',
+      'title' => 'Third composite',
+      'field_composite_entity' => $composite_entity_third,
+    ]);
+    $node->save();
+    $node = $this->getNodeByTitle('Third composite');
+    $node->set('field_composite_entity', NULL);
+    $node->save();
+    $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id());
+    $composite_entity_third->setNewRevision(TRUE);
+    $composite_entity_third->save();
+    $node = $this->getNodeByTitle('Third composite');
+    $node->set('field_composite_entity', $composite_entity_third);
+    $node->save();
+  }
+}
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php
index e92bfbe2b236bfb6d901c7ff333cd0f99bf890a5..ac226fa193e83e102522b04907517d2e336f683d 100644
--- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php
@@ -9,8 +9,8 @@
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
-use Drupal\simpletest\ContentTypeCreationTrait;
-use Drupal\simpletest\NodeCreationTrait;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
 
 /**
  * Tests the entity_reference_revisions composite relationship.
@@ -50,6 +50,13 @@ class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase {
    */
   protected $entityTypeManager;
 
+  /**
+   * The cron service.
+   *
+   * @var \Drupal\Core\Cron
+   */
+  protected $cron;
+
   /**
    * {@inheritdoc}
    */
@@ -79,9 +86,10 @@ protected function setUp() {
     ));
     $field->save();
 
-    // Inject database connection and entity type manager for the tests.
+    // Inject database connection, entity type manager and cron for the tests.
     $this->database = \Drupal::database();
     $this->entityTypeManager = \Drupal::entityTypeManager();
+    $this->cron = \Drupal::service('cron');
   }
 
   /**
@@ -102,53 +110,116 @@ public function testEntityReferenceRevisionsCompositeRelationship() {
     $this->assertEquals(1, $composite_revisions_count);
 
     // Create a node with a reference to the test composite entity.
+    /** @var \Drupal\node\NodeInterface $node */
     $node = Node::create(array(
       'title' => $this->randomMachineName(),
       'type' => 'article',
-      'composite_reference' => $composite,
     ));
     $node->save();
+    $node->set('composite_reference', $composite);
+    $this->assertTrue($node->hasTranslationChanges());
+    $node->save();
 
     // Assert that there is only 1 revision when creating a node.
     $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute();
-    $this->assertEqual($node_revisions_count, 1);
+    $this->assertEquals(1, $node_revisions_count);
     // Assert there is no new composite revision after creating a host entity.
     $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute();
     $this->assertEquals(1, $composite_revisions_count);
 
     // Verify the value of parent type and id after create a node.
     $composite = EntityTestCompositeRelationship::load($composite->id());
-    $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId());
-    $this->assertEqual($composite->parent_id->value, $node->id());
-    $this->assertEqual($composite->parent_field_name->value, 'composite_reference');
+    $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value);
+    $this->assertEquals($node->id(), $composite->parent_id->value);
+    $this->assertEquals('composite_reference', $composite->parent_field_name->value);
     // Create second revision of the node.
     $original_composite_revision = $node->composite_reference[0]->target_revision_id;
     $original_node_revision = $node->getRevisionId();
     $node->setTitle('2nd revision');
     $node->setNewRevision();
     $node->save();
-    $node = node_load($node->id(), TRUE);
+    $node = Node::load($node->id());
     // Check the revision of the node.
-    $this->assertEqual('2nd revision', $node->getTitle(), 'New node revision has changed data.');
-    $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.');
+    $this->assertEquals('2nd revision', $node->getTitle(), 'New node revision has changed data.');
+    $this->assertNotEquals($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.');
 
     // Make sure that there are only 2 revisions.
     $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute();
-    $this->assertEqual($node_revisions_count, 2);
+    $this->assertEquals(2,$node_revisions_count);
 
     // Revert to first revision of the node.
     $node = $this->entityTypeManager->getStorage('node')->loadRevision($original_node_revision);
     $node->setNewRevision();
     $node->isDefaultRevision(TRUE);
     $node->save();
-    $node = node_load($node->id(), TRUE);
+    $node = Node::load($node->id());
     // Check the revision of the node.
-    $this->assertNotEqual('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.');
-    $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.');
+    $this->assertNotEquals('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.');
+    $this->assertNotEquals($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.');
+
+    $node_storage = $this->entityTypeManager->getStorage('node');
+    // Test that removing composite references results in translation changes.
+    $node->set('composite_reference', []);
+    $this->assertTrue($node->hasTranslationChanges());
+
+    // Test that changing composite reference results in translation changes.
+    $changed_composite_reference = $composite;
+    $changed_composite_reference->set('name', 'Changing composite reference');
+    $this->assertTrue((bool) $changed_composite_reference->isRevisionTranslationAffected());
+
+    $node->set('composite_reference', $changed_composite_reference);
+    $node->setNewRevision();
+    $this->assertTrue($node->hasTranslationChanges());
+    $node->save();
+    $nid = $node->id();
+    $node_storage->resetCache([$nid]);
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $node_storage->load($nid);
+
+    // Check the composite has changed.
+    $this->assertEquals('Changing composite reference', $node->get('composite_reference')->entity->getName());
+
+    // Make sure the node has 4 revisions.
+    $node_revisions_count = $node_storage->getQuery()->condition('nid', $nid)->allRevisions()->count()->execute();
+    $this->assertEqual($node_revisions_count, 4);
+
+    // Make sure the node has no revision with revision translation affected
+    // flag set to NULL.
+    $node_revisions_count = $node_storage->getQuery()->condition('nid', $nid)->allRevisions()->condition('revision_translation_affected', NULL, 'IS NULL')->count()->execute();
+    $this->assertEqual($node_revisions_count, 0, 'Node has a revision with revision translation affected set to NULL');
+
+    // Revert the changes to avoid interfering with the delete test.
+    $node->set('composite_reference', $composite);
 
     // Test that the composite entity is deleted when its parent is deleted.
     $node->delete();
+    $this->assertNotNull(EntityTestCompositeRelationship::load($composite->id()));
+
+    $this->cron->run();
     $this->assertNull(EntityTestCompositeRelationship::load($composite->id()));
+
+    // Test that the deleting composite entity does not break the parent entity
+    // when creating a new revision.
+    $composite = EntityTestCompositeRelationship::create([
+      'name' => $this->randomMachineName(),
+    ]);
+    $composite->save();
+    // Create a node with a reference to the test composite entity.
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = Node::create([
+      'title' => $this->randomMachineName(),
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $node->save();
+    // Delete the composite entity.
+    $composite->delete();
+    // Re-apply the field item values to unset the computed "entity" property.
+    $field_item = $node->get('composite_reference')->get(0);
+    $field_item->setValue($field_item->getValue(), FALSE);
+
+    $new_revision = $this->entityTypeManager->getStorage('node')->createRevision($node);
+    $this->assertTrue($new_revision->get('composite_reference')->isEmpty());
   }
 
   /**
@@ -178,23 +249,29 @@ function testCompositeRelationshipWithTranslationNonTranslatableField() {
 
     // Verify the value of parent type and id after create a node.
     $composite = EntityTestCompositeRelationship::load($composite->id());
-    $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId());
-    $this->assertEqual($composite->parent_id->value, $node->id());
-    $this->assertEqual($composite->parent_field_name->value, 'composite_reference');
+    $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value);
+    $this->assertEquals($node->id(), $composite->parent_id->value);
+    $this->assertEquals('composite_reference', $composite->parent_field_name->value);
     $this->assertTrue($composite->hasTranslation('de'));
 
-    // Test that the composite entity is not when the german translation of the
-    // parent is deleted.
+    // Test that the composite entity is not deleted when the german translation
+    // of the parent is deleted.
     $node->removeTranslation('de');
     $node->save();
     $composite = EntityTestCompositeRelationship::load($composite->id());
     $this->assertNotNull($composite);
-    // @todo Support deleting translations of a composite reference.
-    //   @see https://www.drupal.org/node/2834314.
-    //$this->assertFalse($composite->hasTranslation('de'));
+    $this->assertFalse($composite->hasTranslation('de'));
+
+    // Change the language of the entity, ensure that doesn't try to delete
+    // the default translation.
+    $node->set('langcode', 'de');
+    $node->save();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
 
     // Test that the composite entity is deleted when its parent is deleted.
     $node->delete();
+    $this->cron->run();
     $composite = EntityTestCompositeRelationship::load($composite->id());
     $this->assertNull($composite);
   }
@@ -228,23 +305,23 @@ function testCompositeRelationshipWithTranslationTranslatableField() {
 
     // Verify the value of parent type and id after create a node.
     $composite = EntityTestCompositeRelationship::load($composite->id());
-    $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId());
-    $this->assertEqual($composite->parent_id->value, $node->id());
-    $this->assertEqual($composite->parent_field_name->value, 'composite_reference');
+    $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value);
+    $this->assertEquals($node->id(), $composite->parent_id->value);
+    $this->assertEquals('composite_reference', $composite->parent_field_name->value);
 
-    // Test that the composite entity is not when the german translation of the parent is deleted.
+    // Test that the composite entity is not deleted when the German parent
+    // translation is removed.
     $node->removeTranslation('de');
     $node->save();
-    //$this->entityTypeManager->getStorage('entity_test_composite')->resetCache();
+    $this->cron->run();
     $composite = EntityTestCompositeRelationship::load($composite->id());
     $this->assertNotNull($composite);
 
     // Test that the composite entity is deleted when its parent is deleted.
     $node->delete();
+    $this->cron->run();
     $composite = EntityTestCompositeRelationship::load($composite->id());
-    // @todo Support deletions for translatable fields.
-    //   @see https://www.drupal.org/node/2834374
-    // $this->assertNull($composite);
+    $this->assertNull($composite);
   }
 
   /**
@@ -272,15 +349,15 @@ function testCompositeRelationshipWithRevisions() {
     $composite = EntityTestCompositeRelationship::load($composite->id());
     $composite_original_revision_id = $composite->getRevisionId();
     $node_original_revision_id = $node->getRevisionId();
-    $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId());
-    $this->assertEqual($composite->parent_id->value, $node->id());
-    $this->assertEqual($composite->parent_field_name->value, 'composite_reference');
+    $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value);
+    $this->assertEquals($node->id(), $composite->parent_id->value);
+    $this->assertEquals('composite_reference', $composite->parent_field_name->value);
 
     $node->setNewRevision(TRUE);
     $node->save();
     // Ensure that we saved a new revision ID.
     $composite = EntityTestCompositeRelationship::load($composite->id());
-    $this->assertNotEqual($composite->getRevisionId(), $composite_original_revision_id);
+    $this->assertNotEquals($composite_original_revision_id, $composite->getRevisionId());
 
     // Test that deleting the first revision does not delete the composite.
     $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id);
@@ -293,6 +370,7 @@ function testCompositeRelationshipWithRevisions() {
 
     // Test that the composite entity is deleted when its parent is deleted.
     $node->delete();
+    $this->cron->run();
     $composite = EntityTestCompositeRelationship::load($composite->id());
     $this->assertNull($composite);
   }
@@ -400,8 +478,235 @@ function testCompositeRelationshipDuplicatedRevisions() {
 
     // Test that the composite entity is deleted when its parent is deleted.
     $node->delete();
+    $this->cron->run();
     $composite = EntityTestCompositeRelationship::load($composite2->id());
     $this->assertNull($composite);
   }
 
+  /**
+   * Tests the composite entity is deleted after removing its reference.
+   */
+  public function testCompositeDeleteAfterRemovingReference() {
+    list($composite, $node) = $this->assignCompositeToNode();
+
+    // Remove reference to the composite entity from the node.
+    $node->set('composite_reference', NULL);
+    $node->save();
+
+    // Verify that the composite entity is not yet removed after deleting the
+    // parent.
+    $node->delete();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+
+    // Verify that the composite entity is removed after running cron.
+    $this->cron->run();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNull($composite);
+  }
+
+  /**
+   * Tests the composite entity is deleted after removing its reference.
+   *
+   * Includes revisions on the host entity.
+   */
+  public function testCompositeDeleteAfterRemovingReferenceWithRevisions() {
+    list($composite, $node) = $this->assignCompositeToNode();
+
+    // Remove reference to the composite entity from the node in a new revision.
+    $node->set('composite_reference', NULL);
+    $node->setNewRevision();
+    $node->save();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    // Verify the composite entity is not removed on nodes with revisions.
+    $this->assertNotNull($composite);
+
+    // Verify that the composite entity is not yet removed after deleting the
+    // parent.
+    $node->delete();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+
+    // Verify that the composite entity is removed after running cron.
+    $this->cron->run();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNull($composite);
+  }
+
+  /**
+   * Tests the composite entity is not deleted when changing parents.
+   *
+   * Includes revisions on the host entity.
+   */
+  public function testCompositeDeleteAfterChangingParent() {
+    list($composite, $node) = $this->assignCompositeToNode();
+    // Remove reference to the composite entity from the node.
+    $node->set('composite_reference', NULL);
+    $node->setNewRevision();
+    $node->save();
+
+    // Setting a new revision of the composite entity in the second node.
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $composite->setNewRevision(TRUE);
+    $composite->save();
+    $second_node = Node::create([
+      'title' => 'Second node',
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $second_node->save();
+    // Remove reference to the composite entity from the node.
+    $second_node->set('composite_reference', NULL);
+    $second_node->setNewRevision(TRUE);
+    $second_node->save();
+    // Verify the composite entity is not removed on nodes with revisions.
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+    // Verify the amount of revisions of each entity.
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(2, 'node', $node->id());
+    $this->assertRevisionCount(2, 'node', $second_node->id());
+    // Test that the composite entity is not deleted when its new parent is
+    // deleted, since it is still being used in a previous revision with a
+    // different parent.
+    $second_node->delete();
+    $this->cron->run();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+
+    // Delete the parent of the previous revision.
+    $node->delete();
+
+    // Verify that the composite entity is removed after running cron.
+    $this->cron->run();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNull($composite);
+  }
+
+  /**
+   * Composite entity with revisions isn't deleted when changing parents.
+   *
+   * Includes revisions on the host entity.
+   */
+  public function testCompositeDeleteRevisionAfterChangingParent() {
+    list($composite, $node) = $this->assignCompositeToNode();
+    // Remove reference to the composite entity from the node.
+    $node->set('composite_reference', NULL);
+    $node->setNewRevision();
+    $node->save();
+
+    // Setting a new revision of the composite entity in the second node.
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $composite->setNewRevision(TRUE);
+    $composite->save();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $second_node = Node::create([
+      'title' => 'Second node',
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $second_node->save();
+    // Remove reference to the composite entity from the node.
+    $second_node->set('composite_reference', NULL);
+    $second_node->setNewRevision(TRUE);
+    $second_node->save();
+    // Verify the composite entity is not removed on nodes with revisions.
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+    // Verify the amount of revisions of each entity.
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(2, 'node', $node->id());
+    $this->assertRevisionCount(2, 'node', $second_node->id());
+    // Test that the composite entity is not deleted when its old parent is
+    // deleted.
+    $node->delete();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+
+    // Verify that the composite entity is not removed after running cron but
+    // the previous unused revision is deleted.
+    $this->cron->run();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite->id());
+  }
+
+  /**
+   * Tests the composite entity is not deleted when duplicating host entity.
+   *
+   * Includes revisions on the host entity.
+   */
+  public function testCompositeDeleteAfterDuplicatingParent() {
+    list($composite, $node) = $this->assignCompositeToNode();
+    $node->setNewRevision(TRUE);
+    $node->save();
+
+    // Create a duplicate of the node.
+    $duplicate_node = $node->createDuplicate();
+    $duplicate_node->save();
+    $duplicate_node->setNewRevision(TRUE);
+    $duplicate_node->save();
+
+    // Verify the amount of revisions of each entity.
+    $this->assertRevisionCount(3, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(2, 'node', $node->id());
+    $this->assertRevisionCount(2, 'node', $duplicate_node->id());
+    // Test that the composite entity is not deleted when the duplicate is
+    // deleted.
+    $duplicate_node->delete();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+
+    $this->cron->run();
+    $composite = EntityTestCompositeRelationship::load($composite->id());
+    $this->assertNotNull($composite);
+  }
+
+  /**
+   * Asserts the revision count of a certain entity.
+   *
+   * @param int $expected
+   *   The expected count.
+   * @param string $entity_type_id
+   *   The entity type ID, e.g. node.
+   * @param int $entity_id
+   *   The entity ID.
+   */
+  protected function assertRevisionCount($expected, $entity_type_id, $entity_id) {
+    $id_field = \Drupal::entityTypeManager()
+      ->getDefinition($entity_type_id)
+      ->getKey('id');
+    $revision_count = \Drupal::entityQuery($entity_type_id)
+      ->condition($id_field, $entity_id)
+      ->allRevisions()
+      ->count()
+      ->execute();
+    $this->assertEquals($expected, $revision_count);
+  }
+
+  /**
+   * Creates and assigns the composite entity to a node.
+   *
+   * @param string $node_type
+   *   The node type.
+   *
+   * @return array
+   *   An array containing a composite and a node entity.
+   */
+  protected function assignCompositeToNode($node_type = 'article') {
+    $composite = EntityTestCompositeRelationship::create([
+      'uuid' => $this->randomMachineName(),
+      'name' => $this->randomMachineName(),
+    ]);
+    $composite->save();
+    $node = Node::create([
+      'title' => $this->randomMachineName(),
+      'type' => $node_type,
+      'composite_reference' => $composite,
+    ]);
+    $node->save();
+
+    return [$composite, $node];
+  }
+
 }
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..4e261bfcf4385ea6fc5314eb529df414f70304a1
--- /dev/null
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php
@@ -0,0 +1,351 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Tests entity_reference_revisions composites with a translatable field.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsCompositeTranslatableFieldTest extends EntityKernelTestBase {
+
+  use ContentTypeCreationTrait;
+  use NodeCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+    'language',
+    'content_translation'
+  );
+
+  /**
+   * The current database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   *
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    ConfigurableLanguage::createFromLangcode('de')->save();
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+
+    $this->installEntitySchema('entity_test_composite');
+    $this->installSchema('node', ['node_access']);
+
+    // Create article content type.
+    NodeType::create(['type' => 'article', 'name' => 'Article'])->save();
+
+    // Create the reference to the composite entity test.
+    $field_storage = FieldStorageConfig::create(array(
+      'field_name' => 'composite_reference',
+      'entity_type' => 'node',
+      'type' => 'entity_reference_revisions',
+      'settings' => array(
+        'target_type' => 'entity_test_composite'
+      ),
+    ));
+    $field_storage->save();
+    $field = FieldConfig::create(array(
+      'field_storage' => $field_storage,
+      'bundle' => 'article',
+      'translatable' => TRUE,
+    ));
+    $field->save();
+
+    // Inject database connection and entity type manager for the tests.
+    $this->database = \Drupal::database();
+    $this->entityTypeManager = \Drupal::entityTypeManager();
+
+    // @todo content_translation should not be needed for a storage test, but
+    //   \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns
+    //   TRUE if the bundle is explicitly translatable.
+    \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
+    \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE);
+    \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [
+      'untranslatable_fields_hide' => TRUE,
+    ]);
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+  }
+
+  /**
+   * Test the storage for handling pending revisions with translations.
+   */
+  public function testCompositePendingRevisionTranslation() {
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+
+    // Create the test composite entity.
+    $composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Source Composite',
+    ]);
+    $composite->save();
+
+    // Create a node with a reference to the test composite entity.
+    $node = Node::create([
+      'langcode' => 'en',
+      'title' => 'Initial Source Node',
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $node->save();
+
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $node_storage->load($node->id());
+
+    // Assert the revision count.
+    $this->assertRevisionCount(1, 'node', $node->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite->id());
+
+    // Create a translation as a pending revision for both the composite and the
+    // node. While technically, the referenced composite could be the same
+    // entity, for translatable fields, it makes more sense if each translation
+    // points to a separate entity, each only with a single language.
+    $composite_de = $node->get('composite_reference')->entity->createDuplicate();
+    $composite_de->set('langcode', 'de');
+    $composite_de->set('name', 'Pending Revision Composite #1 DE');
+    /** @var \Drupal\node\NodeInterface $node_de */
+    $node_de = $node->addTranslation('de', ['title' => 'Pending Revision Node #1 DE', 'composite_reference' => $composite_de] + $node->toArray());
+    $node_de->setNewRevision(TRUE);
+    $node_de->isDefaultRevision(FALSE);
+    $node_de->save();
+
+    // Assert the revision count.
+    $this->assertRevisionCount(2, 'node', $node->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id());
+
+    // The DE translation will now reference to a pending revision of the
+    // composite entity but the en translation will reference the existing,
+    // unchanged revision.
+    /** @var \Drupal\node\NodeInterface $node_revision */
+    $node_revision = $node_storage->loadRevision($node_de->getRevisionId());
+    $this->assertFalse($node_revision->isDefaultRevision());
+    $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected());
+    $this->assertEquals('Initial Source Node', $node_revision->label());
+    $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label());
+    $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id);
+
+    $node_de = $node_revision->getTranslation('de');
+    $this->assertTrue((bool) $node_de->isRevisionTranslationAffected());
+    $this->assertEquals('Pending Revision Node #1 DE', $node_de->label());
+    // The composite is the default revision because it is a new entity.
+    $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label());
+    $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id);
+
+    // Reload the default revision of the node, make sure that the composite
+    // there is unchanged.
+    $node = $node_storage->load($node->id());
+    $this->assertFalse($node->hasTranslation('de'));
+    $this->assertEquals('Initial Source Node', $node->label());
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label());
+
+    // Create a second translation revision for FR.
+    $composite_fr = $node->get('composite_reference')->entity->createDuplicate();
+    $composite_fr->set('langcode', 'fr');
+    $composite_fr->set('name', 'Pending Revision Composite #1 FR');
+    $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR', 'composite_reference' => $composite_fr] + $node->toArray());
+    $node_fr->setNewRevision(TRUE);
+    $node_fr->isDefaultRevision(FALSE);
+    $node_fr->save();
+
+    // Assert the revision count.
+    $this->assertRevisionCount(3, 'node', $node->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id());
+
+    // Now assert that all 3 revisions exist as expected. Two translation
+    // pending revisions, each has the original revision as parent without
+    // any existing translation.
+    /** @var \Drupal\node\NodeInterface $node_fr */
+    $node_revision = $node_storage->loadRevision($node_fr->getRevisionId());
+    $this->assertFalse($node_revision->isDefaultRevision());
+    $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected());
+    $this->assertEquals('Initial Source Node', $node_revision->label());
+    $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label());
+    $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id);
+
+    $node_fr = $node_revision->getTranslation('fr');
+    $this->assertTrue((bool) $node_fr->isRevisionTranslationAffected());
+    $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label());
+    $this->assertTrue($node_fr->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->label());
+    $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_fr->get('composite_reference')->target_revision_id);
+
+    $node_de = $node_storage->loadRevision($node_de->getRevisionId())->getTranslation('de');
+    $this->assertTrue((bool) $node_de->isRevisionTranslationAffected());
+    $this->assertEquals('Pending Revision Node #1 DE', $node_de->label());
+    $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label());
+    $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id);
+
+    // Reload the default revision of the node, make sure that the composite
+    // there is unchanged.
+    $node = $node_storage->load($node->id());
+    $this->assertFalse($node->hasTranslation('de'));
+    $this->assertEquals('Initial Source Node', $node->label());
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label());
+
+    // Now make a change to the initial source revision, save as a new default
+    // revision.
+    $initial_revision_id = $node->getRevisionId();
+    $node->get('composite_reference')->entity->set('name', 'Updated Source Composite');
+    $node->setTitle('Updated Source Node');
+    $node->setNewRevision(TRUE);
+    $node->save();
+
+    // Assert the revision count.
+    $this->assertRevisionCount(4, 'node', $node->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id());
+
+    // Assert the two english revisions.
+    // Reload the default revision of the node, make sure that the composite
+    // there is unchanged.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->isDefaultRevision());
+    $this->assertFalse($node->hasTranslation('de'));
+    $this->assertFalse($node->hasTranslation('fr'));
+    $this->assertTrue((bool) $node->isRevisionTranslationAffected());
+    $this->assertEquals('Updated Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
+
+    $node_initial = $node_storage->loadRevision($initial_revision_id);
+    $this->assertFalse($node_initial->isDefaultRevision());
+    $this->assertFalse($node_initial->hasTranslation('de'));
+    $this->assertFalse($node_initial->hasTranslation('fr'));
+    $this->assertEquals('Initial Source Node', $node_initial->label());
+    $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label());
+
+    // Now publish the FR pending revision.
+    $node_storage->createRevision($node_fr->getTranslation('fr'))->save();
+
+    // Assert the revision count.
+    $this->assertRevisionCount(5, 'node', $node->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id());
+
+    // The new default revision should now have the updated english source and
+    // the french pending revision.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->isDefaultRevision());
+    $this->assertFalse($node->hasTranslation('de'));
+    $this->assertTrue($node->hasTranslation('fr'));
+    $node_fr = $node->getTranslation('fr');
+    $this->assertFalse((bool) $node->isRevisionTranslationAffected());
+    $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected());
+    $this->assertEquals('Updated Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label());
+    $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
+
+    // Now publish the DE pending revision as well.
+    $node_storage->createRevision($node_de->getTranslation('de'))->save();
+
+    // Assert the revision count.
+    $this->assertRevisionCount(6, 'node', $node->id());
+    $this->assertRevisionCount(2, 'entity_test_composite', $composite->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id());
+    $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id());
+
+    // The new default revision should now have the updated source and both
+    // translations.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->isDefaultRevision());
+    $this->assertTrue($node->hasTranslation('de'));
+    $this->assertTrue($node->hasTranslation('fr'));
+    $node_fr = $node->getTranslation('fr');
+    $node_de = $node->getTranslation('de');
+    $this->assertFalse((bool) $node->isRevisionTranslationAffected());
+    $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected());
+    $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected());
+    $this->assertEquals('Updated Source Node', $node->label());
+
+    // Each translation only has the composite in its translation.
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('en'));
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('en'));
+    $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('en'));
+    $this->assertTrue($node_de->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr'));
+
+    $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label());
+    $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('Pending Revision Node #1 DE', $node_de->label());
+    $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
+  }
+
+  /**
+   * Asserts the revision count of a certain entity.
+   *
+   * @param int $expected
+   *   The expected count.
+   * @param string $entity_type_id
+   *   The entity type ID, e.g. node.
+   * @param int $entity_id
+   *   The entity ID.
+   */
+  protected function assertRevisionCount($expected, $entity_type_id, $entity_id) {
+    $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id');
+
+    $revision_count = \Drupal::entityQuery($entity_type_id)
+      ->condition($id_field, $entity_id)
+      ->allRevisions()
+      ->count()
+      ->execute();
+    $this->assertEquals($expected, $revision_count);
+  }
+
+}
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2a406176ae35ef6e75ddaee7838a1ce2bf0d05b9
--- /dev/null
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php
@@ -0,0 +1,641 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Tests the entity_reference_revisions composite relationship.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsCompositeTranslationTest extends EntityKernelTestBase {
+
+  use ContentTypeCreationTrait;
+  use NodeCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+    'language',
+    'content_translation'
+  ];
+
+  /**
+   * The current database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   *
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    ConfigurableLanguage::createFromLangcode('de')->save();
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+
+    $this->installEntitySchema('entity_test_composite');
+    $this->installSchema('node', ['node_access']);
+
+    // Create article content type.
+    NodeType::create(['type' => 'article', 'name' => 'Article'])->save();
+
+    // Create the reference to the composite entity test.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'composite_reference',
+      'entity_type' => 'node',
+      'type' => 'entity_reference_revisions',
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+      'settings' => [
+        'target_type' => 'entity_test_composite'
+      ],
+    ]);
+    $field_storage->save();
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'article',
+      'translatable' => FALSE,
+    ]);
+    $field->save();
+
+    // Create an untranslatable field on the composite entity.
+    $text_field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_untranslatable',
+      'entity_type' => 'entity_test_composite',
+      'type' => 'string',
+    ]);
+    $text_field_storage->save();
+    $text_field = FieldConfig::create([
+      'field_storage' => $text_field_storage,
+      'bundle' => 'entity_test_composite',
+      'translatable' => FALSE,
+    ]);
+    $text_field->save();
+
+    // Add a nested composite field.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'composite_reference',
+      'entity_type' => 'entity_test_composite',
+      'type' => 'entity_reference_revisions',
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+      'settings' => [
+        'target_type' => 'entity_test_composite'
+      ],
+    ]);
+    $field_storage->save();
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'entity_test_composite',
+      'translatable' => FALSE,
+    ]);
+    $field->save();
+
+    // Inject database connection and entity type manager for the tests.
+    $this->database = \Drupal::database();
+    $this->entityTypeManager = \Drupal::entityTypeManager();
+
+    // @todo content_translation should not be needed for a storage test, but
+    //   \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns
+    //   TRUE if the bundle is explicitly translatable.
+    \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
+    \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE);
+    \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [
+      'untranslatable_fields_hide' => TRUE,
+    ]);
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+  }
+
+  /**
+   * Test the storage for handling pending revisions with translations.
+   */
+  public function testCompositePendingRevisionTranslation() {
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+
+    // Create a nested composite entity.
+    $nested_composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Nested Source Composite',
+    ]);
+    $nested_composite->save();
+
+    // Create a composite entity.
+    $composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Source Composite',
+      'field_untranslatable' => 'Initial untranslatable field',
+      'composite_reference' => $nested_composite,
+    ]);
+    $composite->save();
+
+    // Create a node with a reference to the test composite entity.
+    $node = Node::create([
+      'langcode' => 'en',
+      'title' => 'Initial Source Node',
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $node->save();
+    $initial_revision_id = $node->getRevisionId();
+
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $node_storage->load($node->id());
+
+    // Assert that there is only 1 revision when creating a node.
+    $this->assertRevisionCount(1, $node);
+    // Assert there is no new composite revision after creating a host entity.
+    $this->assertRevisionCount(1, $composite);
+    // Assert there is no new composite revision after creating a host entity.
+    $this->assertRevisionCount(1, $nested_composite);
+
+    // Create a second nested composite entity.
+    $second_nested_composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Nested Composite #2',
+    ]);
+
+    // Add a pending revision.
+    $node = $node_storage->createRevision($node, FALSE);
+    $node->get('composite_reference')->entity->get('composite_reference')->appendItem($second_nested_composite);
+    $node->save();
+    $pending_en_revision_id = $node->getRevisionId();
+
+    $this->assertRevisionCount(2, $node);
+    $this->assertRevisionCount(2, $composite);
+    $this->assertRevisionCount(2, $nested_composite);
+    $this->assertRevisionCount(1, $second_nested_composite);
+
+    // Create a DE translation, start as a draft to replicate the behavior of
+    // the UI.
+    $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray());
+    $node_de = $node_storage->createRevision($node_de, FALSE);
+
+    // Despite starting of the draft revision, creating draft of the translation
+    // uses the paragraphs of the default revision.
+    $this->assertCount(1, $node_de->get('composite_reference')->entity->get('composite_reference'));
+
+    $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE');
+    $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE');
+    $node_de->isDefaultRevision(TRUE);
+    $violations = $node_de->validate();
+    foreach ($violations as $violation) {
+      $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage());
+    }
+    $this->assertEquals(0, count($violations));
+    $node_de->save();
+
+    $this->assertRevisionCount(3, $node);
+    $this->assertRevisionCount(3, $composite);
+    $this->assertRevisionCount(3, $nested_composite);
+    $this->assertRevisionCount(1, $second_nested_composite);
+
+    // Update the translation as a pending revision for both the composite and
+    // the node.
+    $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Revision Composite #1 DE');
+    $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Nested Composite #1 DE');
+    $node_de->set('title', 'Pending Revision Node #1 DE');
+    $node_de->setNewRevision(TRUE);
+    $node_de->isDefaultRevision(FALSE);
+    $violations = $node_de->validate();
+    foreach ($violations as $violation) {
+      $this->fail($violation->getMessage());
+    }
+    $this->assertEquals(0, count($violations));
+    $node_de->save();
+
+    $this->assertRevisionCount(4, $node);
+    $this->assertRevisionCount(4, $composite);
+    $this->assertRevisionCount(4, $nested_composite);
+    $this->assertRevisionCount(1, $second_nested_composite);
+
+    /** @var \Drupal\node\NodeInterface $node_de */
+    $node_de = $node_storage->loadRevision($node_de->getRevisionId());
+    $this->assertFalse($node_de->isDefaultRevision());
+    $this->assertFalse((bool) $node_de->isRevisionTranslationAffected());
+    $this->assertTrue((bool) $node_de->getTranslation('de')->isRevisionTranslationAffected());
+    $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label());
+    $this->assertEquals('Initial Source Node', $node_de->label());
+    $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value);
+    $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label());
+
+    // Reload the default revision of the node, make sure that the composite
+    // there is unchanged.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->hasTranslation('de'));
+    $this->assertEquals('Initial Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label());
+
+    // Create a FR translation, start as a draft to replicate the behavior of
+    // the UI.
+    $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR'] + $node->toArray());
+    $node_fr = $node_storage->createRevision($node_fr, FALSE);
+    $node_fr->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Revision Composite #1 FR');
+    $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Nested Composite #1 FR');
+    $violations = $node_fr->validate();
+    $this->assertEquals(0, count($violations));
+    $node_fr->save();
+
+    // Now assert that all 3 revisions exist as expected. Two translation
+    // pending revisions, each composite has the original revision as parent
+    // without any existing translation.
+    /** @var \Drupal\node\NodeInterface $node_fr */
+    $node_fr = $node_storage->loadRevision($node_fr->getRevisionId());
+    $this->assertFalse($node_fr->isDefaultRevision());
+    $this->assertTrue($node_fr->hasTranslation('de'));
+    $this->assertFalse((bool) $node_fr->isRevisionTranslationAffected());
+    $this->assertTrue((bool) $node_fr->getTranslation('fr')->isRevisionTranslationAffected());
+    $this->assertEquals('Pending Revision Node #1 FR', $node_fr->getTranslation('fr')->label());
+    $this->assertEquals('Initial Source Node', $node_fr->label());
+    $this->assertFalse($node_fr->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('Pending Nested Composite #1 FR', $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->getTranslation('fr')->get('field_untranslatable')->value);
+    $this->assertEquals('Initial Source Composite', $node_fr->get('composite_reference')->entity->label());
+
+    $node_de = $node_storage->loadRevision($node_de->getRevisionId());
+    $this->assertFalse($node_de->isDefaultRevision());
+    $this->assertFalse($node_de->hasTranslation('fr'));
+    $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label());
+    $this->assertEquals('Initial Source Node', $node_de->label());
+    $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value);
+    $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label());
+
+    // Reload the default revision of the node, make sure that the composite
+    // there is unchanged.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->hasTranslation('de'));
+    $this->assertEquals('Initial Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label());
+
+    // Create another pending EN revision and make that the default.
+    $node = $node_storage->loadRevision($pending_en_revision_id);
+    $new_revision = $node_storage->createRevision($node);
+    $new_revision->get('composite_reference')->entity->set('name', 'Updated Source Composite');
+    $new_revision->get('composite_reference')->entity->set('field_untranslatable', 'Updated untranslatable field');
+    $new_revision->setTitle('Updated Source Node');
+    $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->set('name', 'Draft Nested Source Composite #2');
+    $violations = $new_revision->validate();
+    $this->assertEquals(0, count($violations));
+    $new_revision->save();
+
+    // Assert the two english revisions.
+    // Reload the default revision of the node, make sure that the composite
+    // there is unchanged.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->isDefaultRevision());
+    $this->assertTrue($node->hasTranslation('de'));
+    $this->assertFalse($node->hasTranslation('fr'));
+    $this->assertTrue((bool) $node->isRevisionTranslationAffected());
+    $this->assertEquals('Updated Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
+    $this->assertEquals('Initial Nested Source Composite', $node->get('composite_reference')->entity->get('composite_reference')->entity->label());
+    $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label());
+    $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value);
+
+    $node_initial = $node_storage->loadRevision($initial_revision_id);
+    $this->assertFalse($node_initial->isDefaultRevision());
+    $this->assertFalse($node_initial->hasTranslation('de'));
+    $this->assertFalse($node_initial->hasTranslation('fr'));
+    $this->assertEquals('Initial Source Node', $node_initial->label());
+    $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label());
+    $this->assertEquals('Initial Nested Source Composite', $node_initial->get('composite_reference')->entity->get('composite_reference')->entity->label());
+    $this->assertEquals('Initial untranslatable field', $node_initial->get('composite_reference')->entity->get('field_untranslatable')->value);
+    $this->assertCount(1, $node_initial->get('composite_reference')->entity->get('composite_reference'));
+
+    // The current node_fr pending revision still has the initial value before
+    // "merging" it, but it will get the new value for the untranslatable field
+    // in the new revision.
+    $node_fr = $node_storage->loadRevision($node_fr->getRevisionId());
+    $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->get('field_untranslatable')->value);
+    $this->assertCount(1, $node_fr->get('composite_reference')->entity->get('composite_reference'));
+
+    // Now publish the FR pending revision and also add a translation for
+    // the second composite that it now has.
+    $new_revision = $node_storage->createRevision($node_fr->getTranslation('fr'));
+    $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference'));
+    $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->set('name', 'FR Nested Composite #2');
+
+    $violations = $new_revision->validate();
+    $this->assertEquals(0, count($violations));
+    $new_revision->save();
+
+    $this->assertRevisionCount(7, $node);
+    $this->assertRevisionCount(7, $composite);
+    $this->assertRevisionCount(7, $nested_composite);
+    $this->assertRevisionCount(3, $second_nested_composite);
+
+    // The new default revision should now have the updated english source,
+    // original german translation and the french pending revision.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->isDefaultRevision());
+    $this->assertTrue($node->hasTranslation('de'));
+    $this->assertTrue($node->hasTranslation('fr'));
+    $this->assertFalse((bool) $node->isRevisionTranslationAffected());
+    $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected());
+    $this->assertEquals('Updated Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label());
+    $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('Pending Nested Composite #1 FR', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('New Node #1 DE', $node->getTranslation('de')->label());
+    $this->assertEquals('New Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('New Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
+    $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value);
+    $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label());
+    $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label());
+
+    // Now publish the DE pending revision as well.
+    $new_revision = $node_storage->createRevision($node_de->getTranslation('de'));
+    $violations = $new_revision->validate();
+    $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference'));
+    $this->assertEquals(0, count($violations));
+    $new_revision->save();
+
+    $this->assertRevisionCount(8, $node);
+    $this->assertRevisionCount(8, $composite);
+    $this->assertRevisionCount(8, $nested_composite);
+    $this->assertRevisionCount(4, $second_nested_composite);
+
+    // The new default revision should now have the updated source and both
+    // translations.
+    $node = $node_storage->load($node->id());
+    $this->assertTrue($node->isDefaultRevision());
+    $this->assertTrue($node->hasTranslation('de'));
+    $this->assertTrue($node->hasTranslation('fr'));
+    $this->assertFalse((bool) $node->isRevisionTranslationAffected());
+    $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected());
+    $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected());
+    $this->assertEquals('Updated Source Node', $node->label());
+    $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
+    $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr'));
+    $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label());
+    $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label());
+    $this->assertEquals('Pending Revision Node #1 DE', $node->getTranslation('de')->label());
+    $this->assertEquals('Pending Revision Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Pending Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
+    $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
+    $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value);
+    $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label());
+    $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label());
+
+    // The second nested composite of DE inherited the default values for its
+    // translation.
+    $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('de')->label());
+
+    // Simulate creating a new pending revision like
+    // \Drupal\content_moderation\EntityTypeInfo::entityPrepareForm().
+    $new_revision = $node_storage->createRevision($node);
+    $revision_key = $new_revision->getEntityType()->getKey('revision');
+    $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
+    $new_revision->save();
+    $this->assertEquals('Pending Nested Composite #1 DE', $new_revision->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
+
+  }
+
+  /**
+   * Tests that composite translations affects the host entity's translations.
+   */
+  public function testCompositeTranslation() {
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    $node_storage = $this->entityTypeManager->getStorage('node');
+
+    // Create a composite entity.
+    $composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Source Composite',
+    ]);
+    $composite->save();
+
+    // Create a node with a reference to the test composite entity.
+    $node = Node::create([
+      'langcode' => 'en',
+      'title' => 'Initial Source Node',
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $node->save();
+
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $node_storage->load($node->id());
+
+    // Assert that there is only 1 revision when creating a node.
+    $this->assertRevisionCount(1, $node);
+    // Assert that there is only 1 affected revision when creating a node.
+    $this->assertAffectedRevisionCount(1, $node);
+    // Assert there is no new composite revision after creating a host entity.
+    $this->assertRevisionCount(1, $composite);
+
+    $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray());
+    $node_de = $node_storage->createRevision($node_de, FALSE);
+
+    $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE');
+    $node_de->isDefaultRevision(TRUE);
+    $violations = $node_de->validate();
+    foreach ($violations as $violation) {
+      $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage());
+    }
+    $this->assertEquals(0, count($violations));
+    $node_de->save();
+    $this->assertAffectedRevisionCount(1, $node_de);
+    $this->assertAffectedRevisionCount(1, $node);
+
+    // Test that changing composite non default language (DE) reference results
+    // in translation changes for this language but not for the default
+    // language.
+    $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Change Composite #1 DE');
+    $node_de->setNewRevision();
+    $node_de->save();
+
+    $this->assertEquals('Change Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->getName());
+
+    // Make sure the node DE has one more affected translation revision.
+    $this->assertAffectedRevisionCount(2, $node_de);
+    // Make sure the node EN has only one 1 affected translation revision.
+    $this->assertAffectedRevisionCount(1, $node);
+
+    // Test that changing composite in default language (EN) results in
+    // translation changes for this language but not for the DE language.
+    $node = $node_storage->load($node->id());
+    $node->get('composite_reference')->entity->set('name', 'Update Source #1');
+    $node->setNewRevision();
+    $node->save();
+
+    $this->assertEquals('Update Source #1', $node->get('composite_reference')->entity->getTranslation('en')->getName());
+
+    // The node EN now has 2 affected translation revision.
+    $this->assertAffectedRevisionCount(2, $node);
+    // The node DE still has 2 affected translation revisions.
+    $this->assertAffectedRevisionCount(2, $node_de);
+  }
+
+  /**
+   * Tests that nested composite translations affects the host translations.
+   */
+  public function testNestedCompositeTranslation() {
+    /** @var \Drupal\node\NodeStorageInterface $node_storage */
+    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
+
+    // Create a nested composite entity.
+    $nested_composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Nested Source Composite',
+    ]);
+    $nested_composite->addTranslation('de', ['name' => 'Nested Source Composite DE'] + $nested_composite->toArray());
+    $nested_composite->save();
+
+    // Create a composite entity.
+    $composite = EntityTestCompositeRelationship::create([
+      'langcode' => 'en',
+      'name' => 'Initial Source Composite',
+      'field_untranslatable' => 'Initial untranslatable field',
+      'composite_reference' => $nested_composite,
+    ]);
+    $composite->addTranslation('de', ['name' => 'Source Composite DE'] + $composite->toArray());
+    $composite->save();
+
+    // Create a node with a reference to the test composite entity.
+    $node = Node::create([
+      'langcode' => 'en',
+      'title' => 'Initial Source Node',
+      'type' => 'article',
+      'composite_reference' => $composite,
+    ]);
+    $node->save();
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = $node_storage->load($node->id());
+
+    // Assert that there is only 1 revision when creating a node.
+    $this->assertRevisionCount(1, $node);
+    // Assert that there is only 1 affected revision when creating a node.
+    $this->assertAffectedRevisionCount(1, $node);
+    // Assert there is no new composite revision after creating a host entity.
+    $this->assertRevisionCount(1, $composite);
+    // Assert there is no new nested composite revision after creating a host
+    // entity.
+    $this->assertRevisionCount(1, $nested_composite);
+
+    $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray());
+    $node_de = $node_storage->createRevision($node_de, FALSE);
+
+    $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE');
+    $node_de->isDefaultRevision(TRUE);
+    $node_de->save();
+    $this->assertAffectedRevisionCount(1, $node_de);
+    $this->assertAffectedRevisionCount(1, $node);
+
+    // Test that changing nested composite non default language (DE) reference
+    // results in translation changes for this language but not for the default
+    // language.
+    $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->set('name', 'Change Nested Composite #1 DE');
+    $node_de->setNewRevision();
+    $node_de->save();
+
+    $this->assertEquals('Change Nested Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->getName());
+
+    // Make sure the node DE has one more affected translation revision.
+    $this->assertAffectedRevisionCount(2, $node_de);
+    // Make sure the node EN has only one 1 affected translation revision.
+    $this->assertAffectedRevisionCount(1, $node);
+
+    // Test that changing nested composite in default language (EN) results in
+    // translation changes for this language but not for the DE language.
+    $node = $node_storage->load($node->id());
+    $node->get('composite_reference')->entity->get('composite_reference')->entity->set('name', 'Update Nested Source #1');
+    $node->setNewRevision();
+    $node->save();
+
+    $this->assertEquals('Update Nested Source #1', $node->get('composite_reference')->entity->getTranslation('en')->get('composite_reference')->entity->getTranslation('en')->getName());
+
+    // The node EN now has 2 affected translation revision.
+    $this->assertAffectedRevisionCount(2, $node);
+    // The node DE still has 2 affected translation revisions.
+    $this->assertAffectedRevisionCount(2, $node_de);
+  }
+
+  /**
+   * Asserts the affected revision count of a certain entity.
+   *
+   * @param int $expected
+   *   The expected count.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   */
+  protected function assertAffectedRevisionCount($expected, EntityInterface $entity) {
+    $entity_type = $entity->getEntityType();
+    $affected_revisions_count = $this->entityTypeManager->getStorage($entity_type->id())
+      ->getQuery()
+      ->condition($entity_type->getKey('id'), $entity->id())
+      ->condition($entity_type->getKey('langcode'), $entity->language()->getId())
+      ->condition($entity_type->getKey('revision_translation_affected'), 1)
+      ->allRevisions()
+      ->count()
+      ->execute();
+
+    $this->assertEquals($expected, $affected_revisions_count);
+  }
+
+  /**
+   * Asserts the revision count of an entity.
+   *
+   * @param int $expected
+   *   The expected amount of revisions.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   */
+  protected function assertRevisionCount($expected, EntityInterface $entity) {
+    $node_revisions_count = \Drupal::entityQuery($entity->getEntityTypeId())
+      ->condition($entity->getEntityType()->getKey('id'), $entity->id())
+      ->allRevisions()
+      ->count()
+      ->execute();
+    $this->assertEquals($expected, $node_revisions_count);
+  }
+
+}
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php
index 430d06e6209eea49a6acda88403ae28bda24208f..f65e0745d8339bd2f355cdfd95591d95256cad86 100644
--- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php
@@ -8,7 +8,7 @@
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
-use Drupal\simpletest\UserCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
 
 /**
  * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter\EntityReferenceRevisionsEntityFormatter
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php
index e73a2dddc1d1eff63a71a2510ce927d177fc09b7..2b66a63058d9197152193f37a3226d8dc107ff80 100644
--- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php
@@ -88,7 +88,8 @@ public function testNeedsSave() {
       'type' => 'article',
       'composite_reference' => $entity_test,
     ]);
-    // Check the name is properly set.
+    // Check the name is properly set and that getValue() returns the entity
+    // when it is marked as needs save."
     $values = $node->composite_reference->getValue();
     $this->assertTrue(isset($values[0]['entity']));
     static::assertEquals($values[0]['entity']->name->value, $text);
@@ -102,20 +103,22 @@ public function testNeedsSave() {
     static::assertEquals($entity_test_after->name->value, $text);
 
     $new_text = 'Dummy text again';
-    // Set the name again.
-    $entity_test->name = $new_text;
-    $entity_test->setNeedsSave(FALSE);
+    // Set another name and save the node without marking it as needs saving.
+    $entity_test_after->name = $new_text;
+    $entity_test_after->setNeedsSave(FALSE);
 
-    // Load the Node and check the composite reference field is not set.
+    // Load the Node and check the composite reference entity is not returned
+    // from getValue() if it is not marked as needs saving.
     $node = Node::load($node->id());
     $values = $node->composite_reference->getValue();
     $this->assertFalse(isset($values[0]['entity']));
-    $node->composite_reference = $entity_test;
+    $node->composite_reference = $entity_test_after;
     $node->save();
 
     // Check the name is not updated.
+    \Drupal::entityTypeManager()->getStorage('entity_test_composite')->resetCache();
     $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id());
-    static::assertEquals($entity_test_after->name->value, $text);
+    static::assertEquals($text, $entity_test_after->name->value);
 
     // Test if after delete the referenced entity there are no problems setting
     // the referencing values to the parent.
@@ -263,4 +266,56 @@ public function testEntityReferenceRevisionsDefaultValue() {
     $this->assertEquals($dependencies['config'][1], 'node.type.article');
     $this->assertEquals($dependencies['module'][0], 'entity_reference_revisions');
   }
+
+  /**
+   * Tests FieldType\EntityReferenceRevisionsItem::deleteRevision
+   */
+  public function testEntityReferenceRevisionsDeleteHandleDeletedChild() {
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_reference',
+      'entity_type' => 'node',
+      'type' => 'entity_reference_revisions',
+      'settings' => [
+        'target_type' => 'node',
+      ],
+    ]);
+    $field_storage->save();
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'article',
+    ]);
+    $field->save();
+
+    $child = Node::create([
+      'type' => 'article',
+      'title' => 'Child node',
+    ]);
+    $child->save();
+
+    $node = Node::create([
+      'type' => 'article',
+      'title' => 'Parent node',
+      'field_reference' => [
+        [
+          'target_id' => $child->id(),
+          'target_revision_id' => $child->getRevisionId(),
+        ]
+      ],
+    ]);
+
+    // Create two revisions.
+    $node->save();
+    $revisionId = $node->getRevisionId();
+    $node->setNewRevision(TRUE);
+    $node->save();
+
+    // Force delete the child Paragraph.
+    // Core APIs allow this although it is an inconsistent storage situation
+    // for Paragraphs.
+    $child->delete();
+
+    // Previously deleting a revision with a lost child failed fatal.
+    \Drupal::entityTypeManager()->getStorage('node')->deleteRevision($revisionId);
+  }
+
 }
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php
index e68cf058caeb1693c85d9c0e83f65f784c0d2557..86c49a1ffb431f3cef56023cbf6799002124f112 100644
--- a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php
@@ -4,7 +4,6 @@
 
 use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions;
 use Drupal\KernelTests\KernelTestBase;
-use Drupal\migrate\Plugin\MigrationPluginManager;
 use Drupal\migrate\Plugin\MigrateDestinationPluginManager;
 
 /**
@@ -25,7 +24,7 @@ class EntityReferenceRevisionsDeriverTest extends KernelTestBase {
    */
   protected function setUp() {
     parent::setUp();
-    $this->installConfig($this->modules);
+    $this->installConfig(static::$modules);
   }
 
   /**
diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php
index 02e02ec58760a932de36022c457a54506822d0d3..05f781f544756c6ac30373bc1972294fe0a34be4 100644
--- a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php
+++ b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php
@@ -2,8 +2,6 @@
 
 namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\migrate\destination;
 
-use Drupal\Core\Entity\EntityStorageBase;
-use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
@@ -21,9 +19,9 @@
 class EntityReferenceRevisionsDestinationTest extends KernelTestBase implements MigrateMessageInterface {
 
   /**
-   * @var \Drupal\migrate\Plugin\MigrationPluginManager $migrationManager
-   *
    * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManager
    */
   protected $migrationPluginManager;
 
@@ -46,7 +44,7 @@ protected function setUp() {
     parent::setUp();
     $this->installEntitySchema('entity_test_composite');
     $this->installSchema('system', ['sequences']);
-    $this->installConfig($this->modules);
+    $this->installConfig(static::$modules);
 
     $this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
   }
@@ -59,12 +57,12 @@ protected function setUp() {
    * @covers ::getEntityTypeId
    */
   public function testGetEntityTypeId(array $definition, $expected) {
-    /** @var Migration $migration */
+    /** @var \Drupal\migrate\Plugin\Migration $migration */
     $migration = $this->migrationPluginManager->createStubMigration($definition);
-    /** @var EntityReferenceRevisions $destination */
+    /** @var \Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions $destination */
     $destination = $migration->getDestinationPlugin();
 
-    /** @var EntityStorageBase $storage */
+    /** @var \Drupal\Core\Entity\EntityStorageBase $storage */
     $storage = $this->readAttribute($destination, 'storage');
     $actual = $this->readAttribute($storage, 'entityTypeId');
 
@@ -75,13 +73,13 @@ public function testGetEntityTypeId(array $definition, $expected) {
    * Provides multiple migration definitions for "getEntityTypeId" test.
    */
   public function getEntityTypeIdDataProvider() {
-    $datas  = $this->getEntityDataProvider();
+    $data = $this->getEntityDataProvider();
 
-    foreach ($datas as &$data) {
-      $data['expected'] = 'entity_test_composite';
+    foreach ($data as &$datum) {
+      $datum['expected'] = 'entity_test_composite';
     }
 
-    return $datas;
+    return $data;
   }
 
   /**
@@ -94,18 +92,19 @@ public function getEntityTypeIdDataProvider() {
    * @covers ::rollbackNonTranslation
    */
   public function testGetEntity(array $definition, array $expected) {
-    /** @var Migration $migration */
+    /** @var \Drupal\migrate\Plugin\Migration $migration */
     $migration = $this->migrationPluginManager->createStubMigration($definition);
     $migrationExecutable = (new MigrateExecutable($migration, $this));
-    /** @var EntityStorageBase $storage */
+    /** @var \Drupal\Core\Entity\EntityStorageBase $storage */
     $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage');
     // Test inserting and updating by looping twice.
     for ($i = 0; $i < 2; $i++) {
       $migrationExecutable->import();
       $migration->getIdMap()->prepareUpdate();
       foreach ($expected as $data) {
-        $entity = $storage->loadRevision($data['id']);
+        $entity = $storage->loadRevision($data['revision_id']);
         $this->assertEquals($data['label'], $entity->label());
+        $this->assertEquals($data['id'], $entity->id());
       }
     }
     $migrationExecutable->rollback();
@@ -142,12 +141,70 @@ public function getEntityDataProvider() {
           ],
         ],
         'expected' => [
-          ['id' => 1, 'label' => 'content item 1a'],
-          ['id' => 2, 'label' => 'content item 1b'],
-          ['id' => 3, 'label' => 'content item 2'],
+          ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'],
+          ['id' => 2, 'revision_id' => 2, 'label' => 'content item 1b'],
+          ['id' => 3, 'revision_id' => 3, 'label' => 'content item 2'],
+        ],
+      ],
+      'with ids' => [
+        'definition' => [
+          'source' => [
+            'plugin' => 'embedded_data',
+            'data_rows' => [
+              ['id' => 1, 'name' => 'content item 1a'],
+              ['id' => 1, 'name' => 'content item 1b'],
+              ['id' => 2, 'name' => 'content item 2'],
+              ['id' => 3, 'name' => 'content item 3'],
+            ],
+            'ids' => [
+              'id' => ['type' => 'integer'],
+              'name' => ['type' => 'text'],
+            ],
+          ],
+          'process' => [
+            'name' => 'name',
+            'id' => 'id',
+          ],
+          'destination' => [
+            'plugin' => 'entity_reference_revisions:entity_test_composite',
+          ],
+        ],
+        'expected' => [
+          ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1b'],
+          ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'],
+          ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'],
         ],
       ],
-      'with keys' => [
+      'with ids and new revisions' => [
+        'definition' => [
+          'source' => [
+            'plugin' => 'embedded_data',
+            'data_rows' => [
+              ['id' => 1, 'name' => 'content item 1a'],
+              ['id' => 1, 'name' => 'content item 1b'],
+              ['id' => 2, 'name' => 'content item 2'],
+            ],
+            'ids' => [
+              'id' => ['type' => 'integer'],
+              'name' => ['type' => 'text'],
+            ],
+          ],
+          'process' => [
+            'name' => 'name',
+            'id' => 'id',
+          ],
+          'destination' => [
+            'plugin' => 'entity_reference_revisions:entity_test_composite',
+            'new_revisions' => TRUE,
+          ],
+        ],
+        'expected' => [
+          ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'],
+          ['id' => 1, 'revision_id' => 2, 'label' => 'content item 1b'],
+          ['id' => 2, 'revision_id' => 3, 'label' => 'content item 2'],
+        ],
+      ],
+      'with ids and revisions' => [
         'definition' => [
           'source' => [
             'plugin' => 'embedded_data',
@@ -171,9 +228,9 @@ public function getEntityDataProvider() {
           ],
         ],
         'expected' => [
-          ['id' => 1, 'label' => 'content item 1'],
-          ['id' => 2, 'label' => 'content item 2'],
-          ['id' => 3, 'label' => 'content item 3'],
+          ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1'],
+          ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'],
+          ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'],
         ],
       ],
     ];
@@ -183,9 +240,8 @@ public function getEntityDataProvider() {
    * Tests multi-value and single-value destination field linkage.
    *
    * @dataProvider destinationFieldMappingDataProvider
-   *
    */
-  public function testDestinationFieldMapping(array $datas) {
+  public function testDestinationFieldMapping(array $data) {
     $this->enableModules(['node', 'field']);
     $this->installEntitySchema('node');
     $this->installEntitySchema('user');
@@ -202,7 +258,7 @@ public function testDestinationFieldMapping(array $datas) {
       'entity_type' => 'node',
       'type' => 'entity_reference_revisions',
       'settings' => [
-        'target_type' => 'entity_test_composite'
+        'target_type' => 'entity_test_composite',
       ],
       'cardinality' => 1,
     ]);
@@ -219,7 +275,7 @@ public function testDestinationFieldMapping(array $datas) {
       'entity_type' => 'node',
       'type' => 'entity_reference_revisions',
       'settings' => [
-        'target_type' => 'entity_test_composite'
+        'target_type' => 'entity_test_composite',
       ],
       'cardinality' => -1,
     ]);
@@ -232,9 +288,9 @@ public function testDestinationFieldMapping(array $datas) {
 
     $definitions = [];
     $instances = [];
-    foreach ($datas as $data) {
-      $definitions[$data['definition']['id']] = $data['definition'];
-      $instances[$data['definition']['id']] = $this->migrationPluginManager->createStubMigration($data['definition']);
+    foreach ($data as $datum) {
+      $definitions[$datum['definition']['id']] = $datum['definition'];
+      $instances[$datum['definition']['id']] = $this->migrationPluginManager->createStubMigration($datum['definition']);
     }
 
     // Reflection is easier than mocking. We need to use createInstance for
@@ -245,13 +301,13 @@ public function testDestinationFieldMapping(array $datas) {
     $property->setValue($this->migrationPluginManager, $definitions);
     $this->container->set('plugin.manager.migration', $this->migrationPluginManager);
 
-    foreach ($datas as $data) {
-      $migration = $this->migrationPluginManager->createInstance($data['definition']['id']);
+    foreach ($data as $datum) {
+      $migration = $this->migrationPluginManager->createInstance($datum['definition']['id']);
       $migrationExecutable = (new MigrateExecutable($migration, $this));
-      /** @var EntityStorageBase $storage */
+      /** @var \Drupal\Core\Entity\EntityStorageBase $storage */
       $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage');
       $migrationExecutable->import();
-      foreach ($data['expected'] as $expected) {
+      foreach ($datum['expected'] as $expected) {
         $entity = $storage->loadRevision($expected['id']);
         $properties = array_diff_key($expected, array_flip(['id']));
         foreach ($properties as $property => $value) {
@@ -262,7 +318,7 @@ public function testDestinationFieldMapping(array $datas) {
             }
           }
           else {
-            $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] .' is empty');
+            $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] . ' is empty');
             $this->assertEquals($expected[$property], $entity->label());
           }
         }
@@ -405,7 +461,7 @@ public function destinationFieldMappingDataProvider() {
                 ],
                 'field_err_single/target_id' => [
                   [
-                    'plugin' => 'migration',
+                    'plugin' => 'migration_lookup',
                     'migration' => ['single_err'],
                     'no_stub' => TRUE,
                     'source' => 'id',
@@ -419,7 +475,7 @@ public function destinationFieldMappingDataProvider() {
                 ],
                 'field_err_single/target_revision_id' => [
                   [
-                    'plugin' => 'migration',
+                    'plugin' => 'migration_lookup',
                     'migration' => ['single_err'],
                     'no_stub' => TRUE,
                     'source' => 'id',
@@ -433,7 +489,7 @@ public function destinationFieldMappingDataProvider() {
                 ],
                 'field_err_multiple' => [
                   [
-                    'plugin' => 'migration',
+                    'plugin' => 'migration_lookup',
                     'migration' => [
                       'multiple_err_author1',
                       'multiple_err_author2',
@@ -442,7 +498,7 @@ public function destinationFieldMappingDataProvider() {
                     'source' => 'author',
                   ],
                   [
-                    'plugin' => 'iterator',
+                    'plugin' => 'sub_process',
                     'process' => [
                       'target_id' => '0',
                       'target_revision_id' => '1',
diff --git a/web/modules/honeypot/.travis.yml b/web/modules/honeypot/.travis.yml
index 784af29cf4d690ed488f69b3bc20ed672c04ef01..843a0e32958d133a1185fb0fe81607b570c41a9c 100644
--- a/web/modules/honeypot/.travis.yml
+++ b/web/modules/honeypot/.travis.yml
@@ -1,20 +1,13 @@
 ---
 language: php
-php: '7.1'
+php: '7.2'
 services: docker
 
 env:
-  DOCKER_COMPOSE_VERSION: 1.13.0
+  DOCKER_COMPOSE_VERSION: 1.23.2
 
 before_install:
-  # List available docker-engine versions.
-  - apt-cache madison docker-engine
-
-  # Upgrade docker.
-  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
-  - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
-  - sudo apt-get update
-  - sudo apt-get -y install docker-ce
+  - sudo service mysql stop
 
   # Upgrade docker-compose.
   - sudo rm /usr/local/bin/docker-compose
@@ -22,19 +15,33 @@ before_install:
   - chmod +x docker-compose
   - sudo mv docker-compose /usr/local/bin
 
-  # Pull container.
-  - docker pull geerlingguy/drupal-vm:latest
-
-script:
-  # Build environment and install Honeypot.
+install:
+  # Build environment.
   - docker-compose up -d
-  - docker exec honeypot install-drupal
-  - docker exec honeypot ln -s /opt/honeypot/ /var/www/drupalvm/drupal/web/modules/honeypot
-  - docker exec honeypot bash -c 'cd /var/www/drupalvm/drupal/web; drush en -y honeypot simpletest'
 
-  # Fix permissions on the simpletest directories.
-  - docker exec honeypot chown -R www-data:www-data /var/www/drupalvm/drupal/web/sites/simpletest
-  - docker exec honeypot chown -R www-data:www-data /var/www/drupalvm/drupal/web/sites/default/files
+  # Wait for composer create-project to complete.
+  - sleep 300
+
+  # Structure the codebase and install necessary dependencies.
+  - docker-compose exec drupal bash -c 'apt-get update && apt-get install -y sudo'
+  - docker-compose exec drupal bash -c 'composer config platform --unset'
+  - docker-compose exec drupal bash -c 'composer require --dev drush/drush'
+  - docker-compose exec drupal bash -c 'composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies'
+  - docker-compose exec drupal ln -s /opt/honeypot/ /var/www/html/web/modules/honeypot
 
+  # Install Drupal and Honeypot/Testing.
+  - docker-compose exec drupal bash -c 'sudo -u www-data vendor/bin/drush site:install standard --site-name="Honeypot Test" --account-pass admin -y'
+  - docker-compose exec drupal bash -c 'vendor/bin/drush en -y honeypot simpletest'
+
+before_script:
+  # Adjust permissions on the simpletest directories.
+  - docker exec honeypot mkdir -p /var/www/html/web/sites/simpletest
+  - docker exec honeypot chown -R www-data:www-data /var/www/html/web/sites/simpletest
+
+script:
   # Run module tests.
-  - docker exec honeypot bash -c 'sudo -u www-data php /var/www/drupalvm/drupal/web/core/scripts/run-tests.sh --verbose --module honeypot --url http://local.drupalhoneypot.com/'
+  - docker-compose exec drupal bash -c 'sudo -u www-data php web/core/scripts/run-tests.sh --module honeypot --url http://localhost/'
+
+after_failure:
+  # Re-run tests with verbose output for debugging.
+  - docker-compose exec drupal bash -c 'sudo -u www-data php web/core/scripts/run-tests.sh --verbose --module honeypot --url http://localhost/'
diff --git a/web/modules/honeypot/README.md b/web/modules/honeypot/README.md
index 7930749f96f423f6f03f8a0bdd463c400835b5b3..c096502ad351f2e5e2a758c8cf07942c50710fe6 100644
--- a/web/modules/honeypot/README.md
+++ b/web/modules/honeypot/README.md
@@ -43,16 +43,29 @@ restriction on the form by including or not including the option in the array.
 Honeypot includes a `docker-compose.yml` file that can be used for testing purposes. To build a Drupal 8 environment for local testing, do the following:
 
   1. Make sure you have Docker for Mac (or for whatever OS you're using) installed.
-  2. Add the following entry to your `/etc/hosts` file: `192.168.22.33   local.drupalhoneypot.com`
-  3. Run `docker-compose up -d` in this directory.
-  4. Install Drupal: `docker exec honeypot install-drupal` (optionally provide a version after `install-drupal`).
-  5. Link the honeypot module directory into the Drupal modules directory: `docker exec honeypot ln -s /opt/honeypot/ /var/www/drupalvm/drupal/web/modules/honeypot`
-  6. Visit `http://local.drupalhoneypot.com/user` and log in using the admin credentials Drush displayed.
+  1. Run the following commands in this directory to start the environment and install Drush:
 
-> Note: If you're using a Mac, you may also need to perform additional steps to get the hostname working; see [Managing your hosts file](http://docs.drupalvm.com/en/latest/other/docker/#managing-your-hosts-file) in the Drupal VM documentation.
+     ```
+     docker-compose up -d
+     # Wait a couple minutes for the container to build the Drupal codebase.
+     docker-compose exec drupal bash -c 'composer require drush/drush'
+     ```
 
+  1. Link the honeypot module directory into the Drupal modules directory:
+
+     ```
+     docker-compose exec drupal ln -s /opt/honeypot/ /var/www/html/web/modules/honeypot
+     ```
+
+  1. Install Drupal with Drush:
+
+     ```
+     docker-compose exec drupal bash -c 'vendor/bin/drush site:install standard --site-name="Honeypot Test" --account-pass admin -y && chown -R www-data:www-data web/sites/default/files'
+     ```
+
+  1. Log into `http://localhost/` with `admin`/`admin` and enable Honeypot (and the Testing module, if desired).
 
 ## Credit
 
-The Honeypot module was originally developed by Jeff Geerling of Midwestern Mac,
-LLC (midwesternmac.com), and sponsored by Flocknote (flocknote.com).
+The Honeypot module was originally developed by Jeff Geerling of [Midwestern Mac,
+LLC](https://www.midwesternmac.com/), and sponsored by [Flocknote](https://flocknote.com).
diff --git a/web/modules/honeypot/docker-compose.yml b/web/modules/honeypot/docker-compose.yml
index 1104254154475189bf0ccaca42e6654175067d93..1b07b5c0366df106c0a6870c7e378c6dfbf6cef6 100644
--- a/web/modules/honeypot/docker-compose.yml
+++ b/web/modules/honeypot/docker-compose.yml
@@ -1,34 +1,36 @@
 version: "3"
 
 services:
-
-  honeypot:
-    image: geerlingguy/drupal-vm
+  drupal:
+    image: geerlingguy/drupal
     container_name: honeypot
+    environment:
+      DRUPAL_DATABASE_HOST: 'mysql'
+      DRUPAL_DATABASE_PORT: '3306'
+      DRUPAL_DATABASE_NAME: 'drupal'
+      DRUPAL_DATABASE_USERNAME: 'drupal'
+      DRUPAL_DATABASE_PASSWORD: 'drupal'
+      DRUPAL_HASH_SALT: 'fe918c992fb1bcfa01f32303c8b21f3d0a0'
+      DRUPAL_DOWNLOAD_IF_NOT_PRESENT: 'true'
+      DRUPAL_DOWNLOAD_METHOD: 'composer'
+      DRUPAL_PROJECT_ROOT: /var/www/html
+      APACHE_DOCUMENT_ROOT: /var/www/html/web
     ports:
-      - 80:80
-      - 443:443
-    privileged: true
-    extra_hosts:
-      local.drupalhoneypot.com: 127.0.0.1
-    dns:
-      - 8.8.8.8
-      - 8.8.4.4
+      - "80:80"
+    restart: always
     volumes:
-      # Switch to the commented line once Docker CE stable has the feature.
-      - ./:/opt/honeypot/:rw
-      # - ./:/opt/honeypot/:rw,delegated
-    command: /lib/systemd/systemd
-    networks:
-      honeypot:
-        ipv4_address: 192.168.22.33
-
-networks:
+      - ./:/opt/honeypot/:rw,delegated
 
-  honeypot:
-    driver: bridge
-    driver_opts:
-      ip: 192.168.22.1
-    ipam:
-      config:
-        - subnet: "192.168.22.0/16"
+  mysql:
+    image: mysql:5.7
+    container_name: drupal-mysql
+    command: ['--max_allowed_packet=32505856']
+    environment:
+      MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
+      MYSQL_DATABASE: drupal
+      MYSQL_USER: drupal
+      MYSQL_PASSWORD: drupal
+    ports:
+      - "3306:3306"
+    volumes:
+      - /var/lib/mysql
diff --git a/web/modules/honeypot/drupalci.yml b/web/modules/honeypot/drupalci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9adfb33938e5b3d45c3f6c0dea51a36b19e00e84
--- /dev/null
+++ b/web/modules/honeypot/drupalci.yml
@@ -0,0 +1,25 @@
+# Learn to make one for your own drupal.org project:
+# https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing
+build:
+  assessment:
+    validate_codebase:
+      phplint:
+      container_composer:
+      phpcs:
+        # phpcs will use core's specified version of Coder.
+        sniff-all-files: true
+        halt-on-fail: false
+    testing:
+      # run_tests task is executed several times in order of performance speeds.
+      # halt-on-fail can be set on the run_tests tasks in order to fail fast.
+      # suppress-deprecations is false in order to be alerted to usages of
+      # deprecated code.
+      run_tests.standard:
+        types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
+        testgroups: '--all'
+        suppress-deprecations: false
+      run_tests.js:
+        types: 'PHPUnit-FunctionalJavascript'
+        testgroups: '--all'
+        suppress-deprecations: false
+      nightwatchjs: {  }
diff --git a/web/modules/honeypot/honeypot.info.yml b/web/modules/honeypot/honeypot.info.yml
index e7df0fbe9f4711fa732ebd81acef46ad1a427107..177d30a6119df7d3c913476e2fab8a8f8a621b84 100644
--- a/web/modules/honeypot/honeypot.info.yml
+++ b/web/modules/honeypot/honeypot.info.yml
@@ -2,12 +2,11 @@ name: Honeypot
 type: module
 description: 'Mitigates spam form submissions using the honeypot method.'
 package: "Spam control"
-# core: 8.x
+core: 8.x
 configure: honeypot.config
 hidden: false
 
-# Information added by Drupal.org packaging script on 2018-08-09
-version: '8.x-1.28'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2019-12-13
+version: '8.x-1.30'
 project: 'honeypot'
-datestamp: 1533849185
+datestamp: 1576274291
diff --git a/web/modules/honeypot/honeypot.install b/web/modules/honeypot/honeypot.install
index a151b8bb40ea64142a617a9efb1f8214abe8ccfd..dab3049138b0e7037fa79611ede5d5fbae35a3e9 100644
--- a/web/modules/honeypot/honeypot.install
+++ b/web/modules/honeypot/honeypot.install
@@ -48,7 +48,7 @@ function honeypot_schema() {
 function honeypot_install() {
   if (PHP_SAPI !== 'cli') {
     $config_url = Url::fromUri('base://admin/config/content/honeypot');
-    drupal_set_message(t(
+    \Drupal::messenger()->addMessage(t(
       'Honeypot installed successfully. Please <a href=":url">configure Honeypot</a> to protect your forms from spam bots.',
       [':url' => $config_url->toString()]
     ));
diff --git a/web/modules/honeypot/honeypot.module b/web/modules/honeypot/honeypot.module
index 276b834a6628d9e1dc2d9593d9ea0f38b4051830..1da86a6609e083a1d0c0f743cb18c024db29eadc 100644
--- a/web/modules/honeypot/honeypot.module
+++ b/web/modules/honeypot/honeypot.module
@@ -140,6 +140,7 @@ function honeypot_add_form_protection(&$form, FormStateInterface $form_state, ar
     $honeypot_class = $honeypot_element . '-textfield';
     $form[$honeypot_element] = [
       '#theme_wrappers' => [
+        'form_element',
         'container' => [
           '#id' => NULL,
           '#attributes' => [
@@ -168,7 +169,7 @@ function honeypot_add_form_protection(&$form, FormStateInterface $form_state, ar
     $input = $form_state->getUserInput();
     if (empty($input['honeypot_time'])) {
       $identifier = Crypt::randomBytesBase64();
-      \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->set($identifier, time(), 3600*24);
+      \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->setWithExpire($identifier, time(), 3600*24);
     }
     else {
       $identifier = $input['honeypot_time'];
@@ -206,7 +207,7 @@ function _honeypot_honeypot_validate($element, FormStateInterface $form_state) {
   $honeypot_value = $element['#value'];
 
   // Make sure it's empty.
-  if (!empty($honeypot_value)) {
+  if (!empty($honeypot_value) || $honeypot_value == '0') {
     _honeypot_log($form_state->getValue('form_id'), 'honeypot');
     $form_state->setErrorByName('', t('There was a problem with your form submission. Please refresh the page and try again.'));
   }
@@ -239,7 +240,7 @@ function _honeypot_time_restriction_validate($element, FormStateInterface $form_
   if (!$honeypot_time || \Drupal::time()->getRequestTime() < ($honeypot_time + $time_limit)) {
     _honeypot_log($form_state->getValue('form_id'), 'honeypot_time');
     $time_limit = honeypot_get_time_limit();
-    \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->set($identifier, \Drupal::time()->getRequestTime(), 3600*24);
+    \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->setWithExpire($identifier, \Drupal::time()->getRequestTime(), 3600*24);
     $form_state->setErrorByName('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', ['@limit' => $time_limit]));
   }
 }
@@ -294,7 +295,7 @@ function honeypot_get_time_limit(array $form_values = []) {
     $number = $query->countQuery()->execute()->fetchField();
 
     // Don't add more than 30 days' worth of extra time.
-    $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, 2592000);
+    $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, $expire_time);
     // TODO - Only accepts two args.
     $additions = \Drupal::moduleHandler()->invokeAll('honeypot_time_limit', [
       $honeypot_time_limit,
diff --git a/web/modules/honeypot/src/Controller/HoneypotSettingsController.php b/web/modules/honeypot/src/Controller/HoneypotSettingsController.php
index 72e6a4b3761f4bc7e12c3e9c3137cba38ab49b2e..18794d5a2acd857c5d935aa01bddbea6e8e9866d 100644
--- a/web/modules/honeypot/src/Controller/HoneypotSettingsController.php
+++ b/web/modules/honeypot/src/Controller/HoneypotSettingsController.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -47,6 +48,13 @@ class HoneypotSettingsController extends ConfigFormBase {
    */
   protected $cache;
 
+  /**
+   * The Messenger service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
   /**
    * Constructs a settings controller.
    *
@@ -60,13 +68,16 @@ class HoneypotSettingsController extends ConfigFormBase {
    *   The entity type bundle info service.
    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
    *   The cache backend interface.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend, MessengerInterface $messenger) {
     parent::__construct($config_factory);
     $this->moduleHandler = $module_handler;
     $this->entityTypeManager = $entity_type_manager;
     $this->entityTypeBundleInfo = $entity_type_bundle_info;
     $this->cache = $cache_backend;
+    $this->messenger = $messenger;
   }
 
   /**
@@ -78,7 +89,8 @@ public static function create(ContainerInterface $container) {
       $container->get('module_handler'),
       $container->get('entity_type.manager'),
       $container->get('entity_type.bundle.info'),
-      $container->get('cache.default')
+      $container->get('cache.default'),
+      $container->get('messenger')
     );
   }
 
@@ -323,7 +335,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $this->cache->delete('honeypot_protected_forms');
 
     // Tell the user the settings have been saved.
-    drupal_set_message($this->t('The configuration options have been saved.'));
+    $this->messenger->addMessage($this->t('The configuration options have been saved.'));
   }
 
 }
diff --git a/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml b/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml
index c2db19d16ef0bc84ce158a143bf637f4e0951e31..e4c0669576f4c3a2a6774c06bcfdfdbbdbe30c89 100644
--- a/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml
+++ b/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml
@@ -1,12 +1,11 @@
 name: honeypot_test
 type: module
 description: Support module for Honeypot internal testing purposes.
-# core: 8.x
+core: 8.x
 package: Testing
 hidden: true
 
-# Information added by Drupal.org packaging script on 2018-08-09
-version: '8.x-1.28'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2019-12-13
+version: '8.x-1.30'
 project: 'honeypot'
-datestamp: 1533849185
+datestamp: 1576274291
diff --git a/web/modules/honeypot/src/Tests/HoneypotAdminFormTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php
similarity index 71%
rename from web/modules/honeypot/src/Tests/HoneypotAdminFormTest.php
rename to web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php
index 1347d46eff09cafa945b18738f78401d050a7c87..9ae49479ff0498ad1afc9a4f738aa3e6ae01b4e3 100644
--- a/web/modules/honeypot/src/Tests/HoneypotAdminFormTest.php
+++ b/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php
@@ -1,18 +1,30 @@
 <?php
 
-namespace Drupal\honeypot\Tests;
+namespace Drupal\Tests\honeypot\Functional;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Test Honeypot spam protection admin form functionality.
  *
  * @group honeypot
  */
-class HoneypotAdminFormTest extends WebTestBase {
+class HoneypotAdminFormTest extends BrowserTestBase {
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
   protected $adminUser;
 
+  /**
+   * Default theme.
+   *
+   * @var string
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * Modules to enable.
    *
@@ -46,14 +58,14 @@ public function testElementNameUpdateSuccess() {
     $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration'));
 
     // Form should have been submitted successfully.
-    $this->assertText(t('The configuration options have been saved.'), 'Honeypot element name assertion works for valid names.');
+    $this->assertSession()->pageTextContains('The configuration options have been saved.');
 
     // Set up form and submit it.
     $edit['element_name'] = "test-1";
     $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration'));
 
     // Form should have been submitted successfully.
-    $this->assertText(t('The configuration options have been saved.'), 'Honeypot element name assertion works for valid names with dashes and numbers.');
+    $this->assertSession()->pageTextContains('The configuration options have been saved.');
   }
 
   /**
@@ -68,7 +80,7 @@ public function testElementNameUpdateFirstCharacterFail() {
     $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration'));
 
     // Form submission should fail.
-    $this->assertText(t('The element name must start with a letter.'), 'Honeypot element name assertion works for invalid names.');
+    $this->assertSession()->pageTextContains('The element name must start with a letter.');
   }
 
   /**
@@ -83,14 +95,14 @@ public function testElementNameUpdateInvalidCharacterFail() {
     $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration'));
 
     // Form submission should fail.
-    $this->assertText(t('The element name cannot contain spaces or other special characters.'), 'Honeypot element name assertion works for invalid names with special characters.');
+    $this->assertSession()->pageTextContains('The element name cannot contain spaces or other special characters.');
 
     // Set up form and submit it.
     $edit['element_name'] = "space in name";
     $this->drupalPostForm('admin/config/content/honeypot', $edit, t('Save configuration'));
 
     // Form submission should fail.
-    $this->assertText(t('The element name cannot contain spaces or other special characters.'), 'Honeypot element name assertion works for invalid names with spaces.');
+    $this->assertSession()->pageTextContains('The element name cannot contain spaces or other special characters.');
   }
 
 }
diff --git a/web/modules/honeypot/src/Tests/HoneypotFormCacheTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php
similarity index 81%
rename from web/modules/honeypot/src/Tests/HoneypotFormCacheTest.php
rename to web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php
index d899facbac615e8b8c22ed5cd69d97f54f39f73b..69ccb5d1a943957158c908cb557c2f9aed5d0380 100644
--- a/web/modules/honeypot/src/Tests/HoneypotFormCacheTest.php
+++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php
@@ -1,22 +1,38 @@
 <?php
 
-namespace Drupal\honeypot\Tests;
+namespace Drupal\Tests\honeypot\Functional;
 
 use Drupal\comment\Tests\CommentTestTrait;
 use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
 use Drupal\contact\Entity\ContactForm;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
+use Drupal\user\UserInterface;
 
 /**
  * Tests page caching on Honeypot protected forms.
  *
  * @group honeypot
  */
-class HoneypotFormCacheTest extends WebTestBase {
+class HoneypotFormCacheTest extends BrowserTestBase {
 
   use CommentTestTrait;
+
+  /**
+   * Default theme.
+   *
+   * @var string
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Node object.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node;
+
   /**
    * Modules to enable.
    *
@@ -24,8 +40,6 @@ class HoneypotFormCacheTest extends WebTestBase {
    */
   public static $modules = ['honeypot', 'node', 'comment', 'contact'];
 
-  protected $node;
-
   /**
    * {@inheritdoc}
    */
@@ -45,7 +59,7 @@ protected function setUp() {
     // Set up other required configuration.
     $user_config = \Drupal::configFactory()->getEditable('user.settings');
     $user_config->set('verify_mail', TRUE);
-    $user_config->set('register', USER_REGISTER_VISITORS);
+    $user_config->set('register', UserInterface::REGISTER_VISITORS);
     $user_config->save();
 
     // Create an Article node type.
@@ -82,7 +96,7 @@ public function testCacheContactForm() {
 
     // Test on cache header with time limit enabled, cache should miss.
     $this->drupalGet('contact/feedback');
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), '', 'Page was not cached.');
+    $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.');
 
     // Disable time limit.
     \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 0)->save();
@@ -91,12 +105,12 @@ public function testCacheContactForm() {
     $this->drupalGet('contact/feedback');
     // Test on cache header with time limit disabled, cache should hit.
     $this->drupalGet('contact/feedback');
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
+    $this->assertEquals('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was cached.');
 
     // Re-enable the time limit, we should not be seeing the cached version.
     \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 5)->save();
     $this->drupalGet('contact/feedback');
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), '', 'Page was not cached.');
+    $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.');
   }
 
   /**
@@ -120,7 +134,7 @@ public function testCacheCommentForm() {
 
     // Test on cache header with time limit enabled, cache should miss.
     $this->drupalGet('node/' . $this->node->id());
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), '', 'Page was not cached.');
+    $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.');
 
     // Disable time limit.
     \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 0)->save();
@@ -130,7 +144,7 @@ public function testCacheCommentForm() {
 
     // Test on cache header with time limit disabled, cache should hit.
     $this->drupalGet('node/' . $this->node->id());
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
+    $this->assertEquals('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was cached.');
 
   }
 
diff --git a/web/modules/honeypot/src/Tests/HoneypotFormProgrammaticSubmissionTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php
similarity index 64%
rename from web/modules/honeypot/src/Tests/HoneypotFormProgrammaticSubmissionTest.php
rename to web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php
index f1d39fb8daa87f75adf1d04aa9f9b91c7dd81232..50996ecc3cedef548223aa3a34848198cc9062c5 100644
--- a/web/modules/honeypot/src/Tests/HoneypotFormProgrammaticSubmissionTest.php
+++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php
@@ -1,15 +1,23 @@
 <?php
 
-namespace Drupal\honeypot\Tests;
+namespace Drupal\Tests\honeypot\Functional;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\Component\Serialization\Json;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Test programmatic submission of forms protected by Honeypot.
  *
  * @group honeypot
  */
-class HoneypotFormProgrammaticSubmissionTest extends WebTestBase {
+class HoneypotFormProgrammaticSubmissionTest extends BrowserTestBase {
+
+  /**
+   * Default theme.
+   *
+   * @var string
+   */
+  protected $defaultTheme = 'stark';
 
   /**
    * Modules to enable.
@@ -40,9 +48,9 @@ protected function setUp() {
    */
   public function testProgrammaticFormSubmission() {
     $result = $this->drupalGet('/honeypot_test/submit_form');
-    $form_errors = (array) json_decode($result);
-    $this->assertNoRaw('There was a problem with your form submission. Please wait 6 seconds and try again.');
-    $this->assertFalse($form_errors, 'The were no validation errors when submitting the form.');
+    $form_errors = (array) Json::decode($result);
+    $this->assertSession()->responseNotContains('There was a problem with your form submission. Please wait 6 seconds and try again.');
+    $this->assertEmpty($form_errors, 'The were no validation errors when submitting the form.');
   }
 
 }
diff --git a/web/modules/honeypot/src/Tests/HoneypotFormTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php
similarity index 74%
rename from web/modules/honeypot/src/Tests/HoneypotFormTest.php
rename to web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php
index 20ea198375f4a258372713128ef3183c239bb855..e96ccaab9d1cb725da01bb01921664961c4d3863 100644
--- a/web/modules/honeypot/src/Tests/HoneypotFormTest.php
+++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php
@@ -1,25 +1,50 @@
 <?php
 
-namespace Drupal\honeypot\Tests;
+namespace Drupal\Tests\honeypot\Functional;
 
-use Drupal\simpletest\WebTestBase;
 use Drupal\comment\Tests\CommentTestTrait;
 use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
 use Drupal\contact\Entity\ContactForm;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\user\UserInterface;
 
 /**
  * Test Honeypot spam protection functionality.
  *
  * @group honeypot
  */
-class HoneypotFormTest extends WebTestBase {
+class HoneypotFormTest extends BrowserTestBase {
 
   use CommentTestTrait;
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
   protected $adminUser;
+
+  /**
+   * Site visitor.
+   *
+   * @var \Drupal\user\UserInterface
+   */
   protected $webUser;
+
+  /**
+   * Node object.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
   protected $node;
 
+  /**
+   * Default theme.
+   *
+   * @var string
+   */
+  protected $defaultTheme = 'stark';
+
   /**
    * Modules to enable.
    *
@@ -28,7 +53,7 @@ class HoneypotFormTest extends WebTestBase {
   public static $modules = ['honeypot', 'node', 'comment', 'contact'];
 
   /**
-   * Setup before test.
+   * {@inheritdoc}
    */
   public function setUp() {
     // Enable modules required for this test.
@@ -47,7 +72,7 @@ public function setUp() {
     // Set up other required configuration.
     $user_config = \Drupal::configFactory()->getEditable('user.settings');
     $user_config->set('verify_mail', TRUE);
-    $user_config->set('register', USER_REGISTER_VISITORS);
+    $user_config->set('register', UserInterface::REGISTER_VISITORS);
     $user_config->save();
 
     // Create an Article node type.
@@ -89,7 +114,7 @@ public function setUp() {
    */
   public function testUserLoginNotProtected() {
     $this->drupalGet('user');
-    $this->assertNoText('id="edit-url" name="url"', 'Honeypot not enabled on user login form.');
+    $this->assertSession()->responseNotContains('id="edit-url" name="url"');
   }
 
   /**
@@ -102,7 +127,7 @@ public function testProtectRegisterUserNormal() {
     $this->drupalPostForm('user/register', $edit, t('Create new account'));
 
     // Form should have been submitted successfully.
-    $this->assertText(t('A welcome message with further instructions has been sent to your email address.'), 'User registered successfully.');
+    $this->assertSession()->pageTextContains('A welcome message with further instructions has been sent to your email address.');
   }
 
   /**
@@ -116,7 +141,7 @@ public function testProtectUserRegisterHoneypotFilled() {
     $this->drupalPostForm('user/register', $edit, t('Create new account'));
 
     // Form should have error message.
-    $this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), 'Registration form protected by honeypot.');
+    $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.');
   }
 
   /**
@@ -142,7 +167,23 @@ public function testProtectRegisterUserTooFast() {
     $this->drupalPostForm('user/register', $edit, t('Create new account'));
 
     // Form should have error message.
-    $this->assertText(t('There was a problem with your form submission. Please wait 6 seconds and try again.'), 'Registration form protected by time limit.');
+    $this->assertSession()->pageTextContains('There was a problem with your form submission. Please wait 6 seconds and try again.');
+  }
+
+  /**
+   * Test that any (not-strict-empty) value triggers protection.
+   */
+  public function testStrictEmptinessOnHoneypotField() {
+    // Initialise the form values.
+    $edit['name'] = $this->randomMachineName();
+    $edit['mail'] = $edit['name'] . '@example.com';
+
+    // Any value that is not strictly empty should trigger Honeypot.
+    foreach (['0', ' '] as $value) {
+      $edit['url'] = $value;
+      $this->drupalPostForm('user/register', $edit, t('Create new account'));
+      $this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), "Honeypot protection is triggered when the honeypot field contains '{$value}'.");
+    }
   }
 
   /**
@@ -160,7 +201,7 @@ public function testProtectCommentFormNormal() {
     // Set up form and submit it.
     $edit["comment_body[0][value]"] = $comment;
     $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, t('Save'));
-    $this->assertText(t('Your comment has been queued for review'), 'Comment posted successfully.');
+    $this->assertSession()->pageTextContains('Your comment has been queued for review');
   }
 
   /**
@@ -176,7 +217,7 @@ public function testProtectCommentFormHoneypotFilled() {
     $edit["comment_body[0][value]"] = $comment;
     $edit['url'] = 'http://www.example.com/';
     $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, t('Save'));
-    $this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), 'Comment posted successfully.');
+    $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.');
   }
 
   /**
@@ -188,7 +229,7 @@ public function testProtectCommentFormHoneypotBypass() {
 
     // Get the comment reply form and ensure there's no 'url' field.
     $this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment');
-    $this->assertNoText('id="edit-url" name="url"', 'Honeypot home page field not shown.');
+    $this->assertSession()->responseNotContains('id="edit-url" name="url"');
   }
 
   /**
@@ -204,7 +245,7 @@ public function testProtectNodeFormTooFast() {
     // Set up the form and submit it.
     $edit["title[0][value]"] = 'Test Page';
     $this->drupalPostForm('node/add/article', $edit, t('Save'));
-    $this->assertText(t('There was a problem with your form submission.'), 'Honeypot node form timestamp protection works.');
+    $this->assertSession()->pageTextContains('There was a problem with your form submission.');
   }
 
   /**
@@ -217,7 +258,7 @@ public function testProtectNodeFormPreviewPassthru() {
     // Post a node form using the 'Preview' button and make sure it's allowed.
     $edit["title[0][value]"] = 'Test Page';
     $this->drupalPostForm('node/add/article', $edit, t('Preview'));
-    $this->assertNoText(t('There was a problem with your form submission.'), 'Honeypot not blocking node form previews.');
+    $this->assertSession()->pageTextNotContains('There was a problem with your form submission.');
   }
 
   /**
@@ -227,7 +268,10 @@ public function testProtectContactForm() {
     $this->drupalLogin($this->adminUser);
 
     // Disable 'protect_all_forms'.
-    \Drupal::configFactory()->getEditable('honeypot.settings')->set('protect_all_forms', FALSE)->save();
+    \Drupal::configFactory()
+      ->getEditable('honeypot.settings')
+      ->set('protect_all_forms', FALSE)
+      ->save();
 
     // Create a Website feedback contact form.
     $feedback_form = ContactForm::create([
@@ -248,7 +292,7 @@ public function testProtectContactForm() {
 
     $this->drupalLogin($this->webUser);
     $this->drupalGet('contact/feedback');
-    $this->assertField('url', 'Honeypot field is added to Contact form.');
+    $this->assertSession()->fieldExists('url');
   }
 
 }
diff --git a/web/modules/media_entity_twitter/js/twitter.js b/web/modules/media_entity_twitter/js/twitter.js
index 92297172c01cb31a26edb79392c36b2367b61fde..3734e0b9a2968e174c7f3d7060ec1d57252d55e5 100644
--- a/web/modules/media_entity_twitter/js/twitter.js
+++ b/web/modules/media_entity_twitter/js/twitter.js
@@ -2,24 +2,24 @@
  * @file
  */
 
-(function ($, Drupal) {
+(function (Drupal) {
   "use strict";
 
   Drupal.behaviors.twitterMediaEntity = {
     attach: function (context) {
-      function _init () {
-        twttr.widgets.load(context);
+      function _init() {
+        twttr.widgets.load((context && context.nodeType === 1) ? context : null);
       }
 
       // If the tweet is being embedded in a CKEditor's iFrame the widgets
       // library might not have been loaded yet.
       if (typeof twttr == 'undefined') {
-        $.getScript('//platform.twitter.com/widgets.js', _init);
-      }
-      else {
-        _init();
+        var script = document.createElement("script");
+        script.src = '//platform.twitter.com/widgets.js';
+        document.head.appendChild(script);
       }
+      _init();
     }
   };
 
-})(jQuery, Drupal);
+})(Drupal);
diff --git a/web/modules/media_entity_twitter/media_entity_twitter.info.yml b/web/modules/media_entity_twitter/media_entity_twitter.info.yml
index 0fcd6afec420de6d327c26b020c29c50857dd2cd..718e5679d6563122ac39abcc80e823e9b2d2f17f 100644
--- a/web/modules/media_entity_twitter/media_entity_twitter.info.yml
+++ b/web/modules/media_entity_twitter/media_entity_twitter.info.yml
@@ -1,13 +1,12 @@
-name: Media entity Twitter
-description: 'Media entity Twitter provider.'
+name: Media Entity Twitter
+description: 'Media Entity Twitter provider.'
 type: module
 package: Media
-# core: 8.x
+core: 8.x
 dependencies:
   - drupal:media (>= 8.4)
 
-# Information added by Drupal.org packaging script on 2017-10-13
-version: '8.x-2.0-alpha2'
-core: '8.x'
+# Information added by Drupal.org packaging script on 2020-01-21
+version: '8.x-2.3'
 project: 'media_entity_twitter'
-datestamp: 1507907346
+datestamp: 1579597387
diff --git a/web/modules/media_entity_twitter/media_entity_twitter.install b/web/modules/media_entity_twitter/media_entity_twitter.install
index 50be0d8d931e1c4fbf01c94cacfb2d2115bdc288..daa65ab75b33d0f42a3c2f6f3f0796787b98ab61 100644
--- a/web/modules/media_entity_twitter/media_entity_twitter.install
+++ b/web/modules/media_entity_twitter/media_entity_twitter.install
@@ -5,13 +5,15 @@
  * Install, uninstall and update hooks for Media entity Twitter module.
  */
 
+use Drupal\Core\File\FileSystemInterface;
+
 /**
  * Implements hook_install().
  */
 function media_entity_twitter_install() {
   $source = drupal_get_path('module', 'media_entity_twitter') . '/images/icons';
   $destination = \Drupal::config('media.settings')->get('icon_base_uri');
-  file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+  \Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
 
   $files = file_scan_directory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/');
   foreach ($files as $file) {
@@ -22,7 +24,7 @@ function media_entity_twitter_install() {
     // referenced somewhere else. Since showing an error that it was not
     // possible to copy the files is also confusing, we silently do nothing.
     if (!file_exists($destination . DIRECTORY_SEPARATOR . $file->filename)) {
-      file_unmanaged_copy($file->uri, $destination, FILE_EXISTS_ERROR);
+      \Drupal::service('file_system')->copy($file->uri, $destination, FileSystemInterface::EXISTS_ERROR);
     }
   }
 }
@@ -34,7 +36,7 @@ function media_entity_twitter_requirements($phase) {
   $requirements = [];
   if ($phase == 'install') {
     $destination = \Drupal::config('media.settings')->get('icon_base_uri');
-    file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    \Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
     $is_writable = is_writable($destination);
     $is_directory = is_dir($destination);
     if (!$is_writable || !$is_directory) {
diff --git a/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml b/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml
index d22dc483c5589d18815d61d902adde807789e324..e55869159a377234d8515e3d9ef823c57cb643d0 100644
--- a/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml
+++ b/web/modules/media_entity_twitter/media_entity_twitter.libraries.yml
@@ -4,7 +4,6 @@ integration:
     'js/twitter.js': {}
   dependencies:
     - core/drupal
-    - core/jquery
     - media_entity_twitter/twttr.widgets
 twttr.widgets:
   remote: //platform.twitter.com/widgets.js
diff --git a/web/modules/media_entity_twitter/media_entity_twitter.post_update.php b/web/modules/media_entity_twitter/media_entity_twitter.post_update.php
new file mode 100644
index 0000000000000000000000000000000000000000..d61cb74eda23bc5f464a74c368c53c3cbf9ac138
--- /dev/null
+++ b/web/modules/media_entity_twitter/media_entity_twitter.post_update.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Post update functions for Media entity Twitter module.
+ */
+
+/**
+ * Rename the cache bin.
+ */
+function media_entity_twitter_post_update_rename_cache_bin() {
+  // An empty update will force service definitions to be cleared and create a
+  // new bin with new name.
+}
diff --git a/web/modules/media_entity_twitter/media_entity_twitter.services.yml b/web/modules/media_entity_twitter/media_entity_twitter.services.yml
index a520e35817bd62b47114cd77e672e945c2f2e00e..1072dfa0ab3ee287f4afd91094be9c46d7c5cdfe 100644
--- a/web/modules/media_entity_twitter/media_entity_twitter.services.yml
+++ b/web/modules/media_entity_twitter/media_entity_twitter.services.yml
@@ -1,13 +1,13 @@
 services:
   media_entity_twitter.tweet_fetcher:
-    class: '\Drupal\media_entity_twitter\TweetFetcher'
+    class: Drupal\media_entity_twitter\TweetFetcher
     arguments:
-      - '@media_entity_twitter.cache.tweets'
+      - '@cache.tweets'
 
-  media_entity_twitter.cache.tweets:
-    class: '\Drupal\Core\Cache\CacheBackendInterface'
+  cache.tweets:
+    class: Drupal\Core\Cache\CacheBackendInterface
     tags:
-      - { name: cache.bin, default_backend: cache.backend.chainedfast }
+      - { name: cache.bin }
     factory: cache_factory:get
     arguments:
       - tweets
diff --git a/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php b/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php
index ccf13fe12776896fb36dd334167babe37efba014..81967dab07e16aee442b0743cb16c62a3d90904d 100644
--- a/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php
+++ b/web/modules/media_entity_twitter/src/Plugin/Validation/Constraint/TweetVisibleConstraintValidator.php
@@ -3,9 +3,11 @@
 namespace Drupal\media_entity_twitter\Plugin\Validation\Constraint;
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\media_entity_twitter\Plugin\media\Source\Twitter;
 use Drupal\Core\Field\FieldItemInterface;
 use GuzzleHttp\Client;
+use GuzzleHttp\Exception\ClientException;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
@@ -47,7 +49,7 @@ public function validate($value, Constraint $constraint) {
     if (is_string($value)) {
       $data = $value;
     }
-    elseif ($value instanceof FieldItemList) {
+    elseif ($value instanceof FieldItemListInterface) {
       $fieldtype = $value->getFieldDefinition()->getType();
       $field_value = $value->getValue();
       if ($fieldtype == 'link') {
@@ -76,7 +78,13 @@ public function validate($value, Constraint $constraint) {
     }
 
     // Fetch content from the given url.
-    $response = $this->httpClient->get($matches[0][0], ['allow_redirects' => FALSE]);
+    try {
+      $response = $this->httpClient->get($matches[0][0], ['allow_redirects' => FALSE]);
+    }
+    catch (ClientException $e) {
+      $this->context->addViolation($constraint->message);
+      return;
+    }
 
     if ($response->getStatusCode() == 302 && ($location = $response->getHeader('location'))) {
       $effective_url_parts = parse_url($location[0]);
diff --git a/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php b/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php
index 38fdf83324aeae75f710b7acd779e59ef929a90e..a1d994039d8d9d33126b5ab42b7691f4a67bd492 100644
--- a/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php
+++ b/web/modules/media_entity_twitter/src/Plugin/media/Source/Twitter.php
@@ -3,10 +3,13 @@
 namespace Drupal\media_entity_twitter\Plugin\media\Source;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\media\MediaInterface;
 use Drupal\media\MediaSourceBase;
@@ -50,6 +53,13 @@ class Twitter extends MediaSourceBase implements MediaSourceFieldConstraintsInte
    */
   protected $logger;
 
+  /**
+   * The file system service.
+   *
+   * @var \Drupal\Core\File\FileSystemInterface
+   */
+  protected $fileSystem;
+
   /**
    * {@inheritdoc}
    */
@@ -64,7 +74,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $container->get('config.factory'),
       $container->get('renderer'),
       $container->get('media_entity_twitter.tweet_fetcher'),
-      $container->get('logger.factory')->get('media_entity_twitter')
+      $container->get('logger.factory')->get('media_entity_twitter'),
+      $container->get('file_system')
     );
   }
 
@@ -101,11 +112,12 @@ public static function create(ContainerInterface $container, array $configuratio
    * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
    *   The logger channel.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, RendererInterface $renderer, TweetFetcherInterface $tweet_fetcher, LoggerChannelInterface $logger) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, RendererInterface $renderer, TweetFetcherInterface $tweet_fetcher, LoggerChannelInterface $logger, FileSystemInterface $file_system) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
     $this->renderer = $renderer;
     $this->tweetFetcher = $tweet_fetcher;
     $this->logger = $logger;
+    $this->fileSystem = $file_system;
   }
 
   /**
@@ -140,6 +152,8 @@ public function getMetadataAttributes() {
         'content' => $this->t('This tweet content'),
         'retweet_count' => $this->t('Retweet count for this tweet'),
         'profile_image_url_https' => $this->t('Link to profile image'),
+        'created_time' => $this->t('Date/time created'),
+        'user_name' => $this->t('User name'),
       ];
     }
 
@@ -193,7 +207,8 @@ public function getMetadata(MediaInterface $media, $attribute_name) {
         ];
         $svg = $this->renderer->renderRoot($thumbnail);
 
-        return file_unmanaged_save_data($svg, $thumbnail_uri, FILE_EXISTS_ERROR) ?: parent::getMetadata($media, $attribute_name);
+
+        return $this->fileSystem->saveData($svg, $thumbnail_uri, FileSystemInterface::EXISTS_ERROR) ?: parent::getMetadata($media, $attribute_name);
     }
 
     // If we have auth settings return the other fields.
@@ -217,7 +232,7 @@ public function getMetadata(MediaInterface $media, $attribute_name) {
               // @TODO: Use Guzzle, possibly in a service, for this.
               $image_data = file_get_contents($image_url);
               if ($image_data) {
-                return file_unmanaged_save_data($image_data, $local_uri, FILE_EXISTS_REPLACE);
+                return $this->fileSystem->saveData($image_data, $local_uri, FileSystemInterface::EXISTS_REPLACE);
               }
             }
           }
@@ -231,8 +246,8 @@ public function getMetadata(MediaInterface $media, $attribute_name) {
           return NULL;
 
         case 'content':
-          if (isset($tweet['text'])) {
-            return $tweet['text'];
+          if (isset($tweet['full_text'])) {
+            return $tweet['full_text'];
           }
           return NULL;
 
@@ -248,6 +263,20 @@ public function getMetadata(MediaInterface $media, $attribute_name) {
           }
           return NULL;
 
+        case 'created_time':
+          if (isset($tweet['created_at'])) {
+            if ($datetime = DrupalDateTime::createFromFormat('D M d H:i:s O Y', $tweet['created_at'])) {
+              return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
+            }
+          }
+          return NULL;
+
+        case 'user_name':
+          if (isset($tweet['user']['name'])) {
+            return $tweet['user']['name'];
+          }
+          return NULL;
+
         case 'default_name':
           $user = $this->getMetadata($media, 'user');
           $id = $this->getMetadata($media, 'id');
@@ -380,7 +409,7 @@ protected function getLocalImageUri($id, MediaInterface $media, $media_url = NUL
 
     // Ensure that the destination directory is writable. If not, log a warning
     // and return the default thumbnail.
-    $ready = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    $ready = $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
     if (!$ready) {
       $this->logger->warning('Could not prepare thumbnail destination directory @dir', [
         '@dir' => $directory,
diff --git a/web/modules/media_entity_twitter/src/TweetFetcher.php b/web/modules/media_entity_twitter/src/TweetFetcher.php
index 88b442bd7eca6584640b5770aade238430022374..6c72f99a55fc0acb321f75b7189aa52760c76cdf 100644
--- a/web/modules/media_entity_twitter/src/TweetFetcher.php
+++ b/web/modules/media_entity_twitter/src/TweetFetcher.php
@@ -59,7 +59,7 @@ public function fetchTweet($id) {
 
     // Query Twitter's API.
     $response = $this->twitter
-      ->setGetfield('?id=' . $id)
+      ->setGetfield('?id=' . $id . '&tweet_mode=extended')
       ->buildOAuth('https://api.twitter.com/1.1/statuses/show.json', 'GET')
       ->performRequest();
 
diff --git a/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php b/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php
index bfdeca2b6ce569801e5fb537eb1a8f323a5ee42c..8470b3a36b17f59a071ed571e2a4b8004d543511 100644
--- a/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php
+++ b/web/modules/media_entity_twitter/tests/src/Functional/TweetEmbedFormatterTest.php
@@ -21,19 +21,12 @@ class TweetEmbedFormatterTest extends MediaFunctionalTestBase {
     'link',
   ];
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-  }
-
   /**
    * Tests adding and editing a twitter embed formatter.
    */
   public function testManageEmbedFormatter() {
     // Test and create one media type.
-    $bundle = $this->createMediaType(['bundle' => 'twitter'], 'twitter');
+    $bundle = $this->createMediaType('twitter', ['id' => 'twitter']);
 
     // We need to fix widget and formatter config for the default field.
     $source = $bundle->getSource();
@@ -42,6 +35,13 @@ public function testManageEmbedFormatter() {
     $component = \Drupal::service('plugin.manager.field.widget')
       ->prepareConfiguration('string', []);
 
+    // Enable the conical URL.
+    \Drupal::configFactory()
+      ->getEditable('media.settings')
+      ->set('standalone_url', TRUE)
+      ->save(TRUE);
+    $this->container->get('router.builder')->rebuild();
+
     // @todo Replace entity_get_form_display() when #2367933 is done.
     // https://www.drupal.org/node/2872159.
     entity_get_form_display('media', $bundle->id(), 'default')
@@ -50,8 +50,8 @@ public function testManageEmbedFormatter() {
 
     // Assert that the media type has the expected values before proceeding.
     $this->drupalGet('admin/structure/media/manage/' . $bundle->id());
-    $this->assertFieldByName('label', $bundle->label());
-    $this->assertFieldByName('source', 'twitter');
+    $this->assertSession()->fieldValueEquals('label', $bundle->label());
+    $this->assertSession()->fieldValueEquals('source', 'twitter');
 
     // Add and save string_long field type settings (Embed code).
     $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/fields/add-field');
@@ -61,33 +61,36 @@ public function testManageEmbedFormatter() {
       'field_name' => 'embed_code',
     ];
     $this->drupalPostForm(NULL, $edit_conf, t('Save and continue'));
-    $this->assertText('These settings apply to the ' . $edit_conf['label'] . ' field everywhere it is used.');
+    $this->assertSession()
+        ->responseContains('These settings apply to the <em class="placeholder">' . $edit_conf['label'] . '</em> field everywhere it is used.');
     $edit = [
       'cardinality' => 'number',
       'cardinality_number' => '1',
     ];
     $this->drupalPostForm(NULL, $edit, t('Save field settings'));
-    $this->assertText('Updated field ' . $edit_conf['label'] . ' field settings.');
+    $this->assertSession()
+        ->responseContains('Updated field <em class="placeholder">' . $edit_conf['label'] . '</em> field settings.');
 
     // Set the new string_long field type as required.
     $edit = [
       'required' => TRUE,
     ];
     $this->drupalPostForm(NULL, $edit, t('Save settings'));
-    $this->assertText('Saved ' . $edit_conf['label'] . ' configuration.');
+    $this->assertSession()
+        ->responseContains('Saved <em class="placeholder">' . $edit_conf['label'] . '</em> configuration.');
 
     // Assert that the new field types configurations have been successfully
     // saved.
     $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/fields');
     $xpath = $this->xpath('//*[@id=:id]/td', [':id' => 'field-media-twitter']);
-    $this->assertEqual((string) $xpath[0]->getText(), 'Tweet Url');
-    $this->assertEqual((string) $xpath[1]->getText(), 'field_media_twitter');
-    $this->assertEqual((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain)');
+    $this->assertEquals((string) $xpath[0]->getText(), 'Tweet URL');
+    $this->assertEquals((string) $xpath[1]->getText(), 'field_media_twitter');
+    $this->assertEquals((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain)');
 
     $xpath = $this->xpath('//*[@id=:id]/td', [':id' => 'field-embed-code']);
-    $this->assertEqual((string) $xpath[0]->getText(), 'Embed code');
-    $this->assertEqual((string) $xpath[1]->getText(), 'field_embed_code');
-    $this->assertEqual((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain, long)');
+    $this->assertEquals((string) $xpath[0]->getText(), 'Embed code');
+    $this->assertEquals((string) $xpath[1]->getText(), 'field_embed_code');
+    $this->assertEquals((string) $xpath[2]->find('css', 'a')->getText(), 'Text (plain, long)');
 
     $this->drupalGet('admin/structure/media/manage/' . $bundle->id() . '/display');
 
@@ -101,13 +104,13 @@ public function testManageEmbedFormatter() {
       'fields[field_embed_code][type]' => 'twitter_embed',
     ];
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    $this->assertText('Your settings have been saved.');
+    $this->assertSession()->responseContains('Your settings have been saved.');
 
     // Create and save the media with a twitter media code.
     $this->drupalGet('media/add/' . $bundle->id());
 
     // Random image url from twitter.
-    $tweet_url = 'https://twitter.com/RamzyStinson/status/670650348319576064';
+    $tweet_url = 'https://twitter.com/DrupalConEur/status/1176518741208817664';
 
     // Random image from twitter.
     $tweet = '<blockquote class="twitter-tweet" lang="it"><p lang="en" dir="ltr">' .
@@ -123,17 +126,19 @@ public function testManageEmbedFormatter() {
       'field_embed_code[0][value]' => $tweet,
     ];
     $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->drupalGet('media/1');
 
     // Assert that the media has been successfully saved.
-    $this->assertText('Title');
+    $this->assertSession()->pageTextContains('Title');
 
     // Assert that the link url formatter exists on this page.
-    $this->assertText('Tweet Url');
-    $this->assertRaw('<a href="https://twitter.com/RamzyStinson/statuses/670650348319576064">', 'Link in embedded Tweet found.');
+    $this->assertSession()->pageTextContains('Tweet URL');
+    $this->assertSession()
+        ->responseContains('<a href="https://twitter.com/RamzyStinson/statuses/670650348319576064">', 'Link in embedded Tweet found.');
 
     // Assert that the string_long code formatter exists on this page.
-    $this->assertText('Embed code');
-    $this->assertRaw('<blockquote class="twitter-tweet', 'Embedded Tweet found.');
+    $this->assertSession()->pageTextContains('Embed code');
+    $this->assertSession()->responseContains('<blockquote class="twitter-tweet', 'Embedded Tweet found.');
   }
 
 }
diff --git a/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php b/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php
index a39cda295b4bb436238d40243ab889b954949d90..4c43d24c816980b5e9db62234a46c6837ff1075d 100644
--- a/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php
+++ b/web/modules/media_entity_twitter/tests/src/Kernel/ThumbnailTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\media_entity_twitter\Kernel;
 
+use Drupal\Core\File\FileSystemInterface;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
@@ -61,7 +62,7 @@ protected function setUp() {
     $this->installEntitySchema('media');
     $this->installConfig(['media_entity_twitter', 'system']);
 
-    $this->tweetFetcher = $this->getMock(TweetFetcherInterface::class);
+    $this->tweetFetcher = $this->createMock(TweetFetcherInterface::class);
     $this->container->set('media_entity_twitter.tweet_fetcher', $this->tweetFetcher);
 
     MediaType::create([
@@ -106,7 +107,7 @@ protected function setUp() {
       ->get('media_entity_twitter.settings')
       ->get('local_images');
 
-    file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    \Drupal::service('file_system')->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY| FileSystemInterface::MODIFY_PERMISSIONS);
   }
 
   /**
diff --git a/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php b/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php
index fdbc1148ce025f1f7b31dabc832b634ff532f831..69e4a1bb0f641ec187b72a584615b4c626625720 100644
--- a/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php
+++ b/web/modules/media_entity_twitter/tests/src/Unit/ConstraintsTest.php
@@ -96,7 +96,7 @@ public function testTweetVisibleConstraint($embed_code, $mocked_response, $viola
     $constraint = new TweetVisibleConstraint();
     $this->assertEquals('Referenced tweet is not publicly visible.', $constraint->message, 'Correct constraint message found.');
 
-    $http_client = $this->getMock('\GuzzleHttp\Client');
+    $http_client = $this->createMock('\GuzzleHttp\Client');
     $http_client->expects($this->once())
       ->method('__call')
       ->with('get', [$embed_code, ['allow_redirects' => FALSE]])
@@ -127,12 +127,12 @@ public function testTweetVisibleConstraint($embed_code, $mocked_response, $viola
    * Provides test data for testTweetVisibleConstraint().
    */
   public function visibleProvider() {
-    $visible_response = $this->getMock('\GuzzleHttp\Psr7\Response');
+    $visible_response = $this->createMock('\GuzzleHttp\Psr7\Response');
     $visible_response->expects($this->any())
       ->method('getStatusCode')
       ->will($this->returnValue(200));
 
-    $invisible_response = $this->getMock('\GuzzleHttp\Psr7\Response');
+    $invisible_response = $this->createMock('\GuzzleHttp\Psr7\Response');
     $invisible_response->expects($this->once())
       ->method('getStatusCode')
       ->will($this->returnValue(302));
@@ -165,8 +165,10 @@ public function visibleProvider() {
    */
   public function testBadUrlsOnVisibleConstraint($embed_code) {
 
-    $http_client = $this->getMock('\GuzzleHttp\Client');
-    $http_client->expects($this->never())->method('get');
+    $http_client = $this->createMock('\GuzzleHttp\Client');
+    $http_client->expects($this->never())
+      ->method('__call')
+      ->with('get', [$embed_code, ['allow_redirects' => FALSE]]);
 
     $execution_context = $this->getMockBuilder('\Drupal\Core\TypedData\Validation\ExecutionContext')
       ->disableOriginalConstructor()