From 6e05a2036d5c74035ff8d75e6950bcc5ea304de1 Mon Sep 17 00:00:00 2001
From: Brian Canini <canini.16@osu.edu>
Date: Tue, 24 Mar 2020 15:13:30 -0400
Subject: [PATCH] updating entity_reference_revisions from 1.3.0 => 1.8.0

---
 composer.json                                 |   4 +-
 composer.lock                                 |  33 +-
 vendor/composer/installed.json                |  30 +-
 .../entity_reference_revisions/composer.json  |  12 +
 .../entity_reference_revisions.info.yml       |  12 +-
 .../entity_reference_revisions.links.menu.yml |   6 +
 .../entity_reference_revisions.module         | 131 ++++
 ...entity_reference_revisions.permissions.yml |   3 +
 .../entity_reference_revisions.routing.yml    |   7 +
 .../entity_reference_revisions.services.yml   |   4 +
 .../entity_reference_revisions.views.inc      |   2 +-
 .../EntityReferenceRevisionsFieldItemList.php |  26 +
 .../EntityReferenceRevisionsOrphanPurger.php  | 370 ++++++++++
 .../OrphanedCompositeEntitiesDeleteForm.php   | 130 ++++
 .../DataType/EntityReferenceRevisions.php     |   9 +-
 .../EntityReferenceRevisionsItem.php          | 125 +++-
 ...tyReferenceRevisionsAutocompleteWidget.php |   1 -
 .../src/Plugin/QueueWorker/OrphanPurger.php   | 113 +++
 .../destination/EntityReferenceRevisions.php  |  62 +-
 .../display/EntityReferenceRevisions.php      |   5 +-
 ...ntity_composite_relationship_test.info.yml |  14 +-
 .../EntityTestCompositeRelationship.php       |  10 +-
 .../EntityReferenceRevisionsAdminTest.php     |  63 +-
 ...tityReferenceRevisionsAutocompleteTest.php |  15 +-
 .../EntityReferenceRevisionsDiffTest.php      |  24 +-
 ...EntityReferenceRevisionsNormalizerTest.php |  19 +-
 ...ityReferenceRevisionsOrphanRemovalTest.php | 371 ++++++++++
 .../EntityReferenceRevisionsCompositeTest.php | 375 +++++++++-
 ...evisionsCompositeTranslatableFieldTest.php | 351 ++++++++++
 ...renceRevisionsCompositeTranslationTest.php | 641 ++++++++++++++++++
 .../EntityReferenceRevisionsFormatterTest.php |   2 +-
 .../EntityReferenceRevisionsSaveTest.php      |  69 +-
 .../EntityReferenceRevisionsDeriverTest.php   |   3 +-
 ...ntityReferenceRevisionsDestinationTest.php | 132 ++--
 34 files changed, 2983 insertions(+), 191 deletions(-)
 create mode 100644 web/modules/entity_reference_revisions/composer.json
 create mode 100644 web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml
 create mode 100644 web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml
 create mode 100644 web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml
 create mode 100755 web/modules/entity_reference_revisions/entity_reference_revisions.services.yml
 create mode 100644 web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php
 create mode 100644 web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php
 create mode 100644 web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php
 rename web/modules/entity_reference_revisions/{src/Tests => tests/src/Functional}/EntityReferenceRevisionsAdminTest.php (74%)
 rename web/modules/entity_reference_revisions/{src/Tests => tests/src/Functional}/EntityReferenceRevisionsAutocompleteTest.php (92%)
 rename web/modules/entity_reference_revisions/{src/Tests => tests/src/Functional}/EntityReferenceRevisionsDiffTest.php (83%)
 rename web/modules/entity_reference_revisions/{src/Tests => tests/src/Functional}/EntityReferenceRevisionsNormalizerTest.php (90%)
 create mode 100644 web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php
 create mode 100644 web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php
 create mode 100644 web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php

diff --git a/composer.json b/composer.json
index 1a392f6d64..639831a05d 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
@@ -318,4 +318,4 @@
             "php": "7.0.8"
         }
     }
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index fd58f43968..4d0ada5e3e 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": "99fd23635d971fe3335373bc7bbfb9d2",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -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"
             }
         },
         {
@@ -6064,8 +6068,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 1dcf950ce4..302b90d8cf 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -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"
         }
     },
     {
diff --git a/web/modules/entity_reference_revisions/composer.json b/web/modules/entity_reference_revisions/composer.json
new file mode 100644
index 0000000000..7fdfb2e919
--- /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 6c425b7f11..55807d4348 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 0000000000..beb8161771
--- /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 ba9ac496f9..fa24f77eab 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 0000000000..bcd64c16d3
--- /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 0000000000..cd90db817d
--- /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 0000000000..d112df982e
--- /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 9d351b3765..6dd8ec66f7 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 ba907277ad..983e7be269 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 0000000000..8735817fc0
--- /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 0000000000..b3da36da6b
--- /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 c9e66ac665..b24b6b1076 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 cdf2867b1b..16496dbc7e 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 faad139aba..c1d5ee51d9 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 0000000000..a2bf051f1b
--- /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 a50f5a7675..d4baf31c65 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 3c738564d2..65ad121f9b 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 d81f3c3672..7b906fc29d 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 fb61ad1594..9ef825c409 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 e0a4c59274..f2618b914f 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 4998a41cee..95cff09154 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 e8ea4ad0c6..a79d4d94db 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 3faf8fb41a..740fe8b192 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 0000000000..3fa8798747
--- /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 e92bfbe2b2..ac226fa193 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 0000000000..4e261bfcf4
--- /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 0000000000..2a406176ae
--- /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 430d06e620..f65e0745d8 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 e73a2dddc1..2b66a63058 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 e68cf058ca..86c49a1ffb 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 02e02ec587..05f781f544 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',
-- 
GitLab