From 6165bb1503bfdbbdcbb6623ea814bc034ef43c1b Mon Sep 17 00:00:00 2001
From: Kirill Roskolii <kirill.roskolii@catalyst.net.nz>
Date: Mon, 24 Feb 2025 10:50:47 +1300
Subject: [PATCH 1/4] Support multi-valued fields

---
 phpstan-baseline.neon                       |   6 -
 src/DiffEntityComparison.php                |  10 +-
 src/Plugin/diff/Field/ImageFieldBuilder.php |  27 +++--
 tests/src/Functional/DiffPluginFileTest.php | 123 ++++++++++++++------
 4 files changed, 110 insertions(+), 56 deletions(-)

diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 6c8a18c..45a349d 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -345,11 +345,6 @@ parameters:
 			count: 1
 			path: src/Plugin/diff/Field/ImageFieldBuilder.php
 
-		-
-			message: "#^Implicit array creation is not allowed \\- variable \\$image_style might not exist\\.$#"
-			count: 1
-			path: src/Plugin/diff/Field/ImageFieldBuilder.php
-
 		-
 			message: "#^Method Drupal\\\\diff\\\\Plugin\\\\diff\\\\Field\\\\ImageFieldBuilder\\:\\:build\\(\\) has parameter \\$field_items with generic interface Drupal\\\\Core\\\\Field\\\\FieldItemListInterface but does not specify its types\\: T$#"
 			count: 1
@@ -575,4 +570,3 @@ parameters:
 			message: "#^Property Drupal\\\\Tests\\\\diff\\\\Unit\\\\VisualDiffThemeNegotiatorTest\\:\\:\\$configFactory with generic interface Prophecy\\\\Prophecy\\\\ProphecyInterface does not specify its types\\: T$#"
 			count: 1
 			path: tests/src/Unit/VisualDiffThemeNegotiatorTest.php
-
diff --git a/src/DiffEntityComparison.php b/src/DiffEntityComparison.php
index c8c7a03..25668a9 100644
--- a/src/DiffEntityComparison.php
+++ b/src/DiffEntityComparison.php
@@ -116,19 +116,17 @@ class DiffEntityComparison {
         $value = $left_values[$delta];
         if (isset($value['#thumbnail'])) {
           $result['#left_thumbnail'][] = $value['#thumbnail'];
+          $value = $value['data'];
         }
-        else {
-          $result['#left'][] = \is_array($value) ? \implode("\n", $value) : $value;
-        }
+        $result['#left'][] = \is_array($value) ? \implode("\n", $value) : $value;
       }
       if (isset($right_values[$delta])) {
         $value = $right_values[$delta];
         if (isset($value['#thumbnail'])) {
           $result['#right_thumbnail'][] = $value['#thumbnail'];
+          $value = $value['data'];
         }
-        else {
-          $result['#right'][] = \is_array($value) ? \implode("\n", $value) : $value;
-        }
+        $result['#right'][] = \is_array($value) ? \implode("\n", $value) : $value;
       }
     }
 
diff --git a/src/Plugin/diff/Field/ImageFieldBuilder.php b/src/Plugin/diff/Field/ImageFieldBuilder.php
index 24b920d..b9264bd 100644
--- a/src/Plugin/diff/Field/ImageFieldBuilder.php
+++ b/src/Plugin/diff/Field/ImageFieldBuilder.php
@@ -30,13 +30,14 @@ class ImageFieldBuilder extends FieldDiffBuilderBase {
     // Every item from $field_items is of type FieldItemInterface.
     foreach ($field_items as $field_key => $field_item) {
       if (!$field_item->isEmpty()) {
+        $item_data = [];
         $values = $field_item->getValue();
 
         // Compare file names.
         if (isset($values['target_id'])) {
           /** @var \Drupal\file\Entity\File $image */
           $image = $fileManager->load($values['target_id']);
-          $result[$field_key][] = $this->t('Image: @image', [
+          $item_data[] = (string) $this->t('Image: @image', [
             '@image' => $image->getFilename(),
           ]);
         }
@@ -44,7 +45,7 @@ class ImageFieldBuilder extends FieldDiffBuilderBase {
         // Compare Alt fields.
         if ($this->configuration['compare_alt_field']) {
           if (isset($values['alt'])) {
-            $result[$field_key][] = $this->t('Alt: @alt', [
+            $item_data[] = (string) $this->t('Alt: @alt', [
               '@alt' => $values['alt'],
             ]);
           }
@@ -53,7 +54,7 @@ class ImageFieldBuilder extends FieldDiffBuilderBase {
         // Compare Title fields.
         if ($this->configuration['compare_title_field']) {
           if (!empty($values['title'])) {
-            $result[$field_key][] = $this->t('Title: @title', [
+            $item_data[] = (string) $this->t('Title: @title', [
               '@title' => $values['title'],
             ]);
           }
@@ -62,32 +63,38 @@ class ImageFieldBuilder extends FieldDiffBuilderBase {
         // Compare file id.
         if ($this->configuration['show_id']) {
           if (isset($values['target_id'])) {
-            $result[$field_key][] = $this->t('File ID: @fid', [
+            $item_data[] = (string) $this->t('File ID: @fid', [
               '@fid' => $values['target_id'],
             ]);
           }
         }
 
-        $separator = $this->configuration['property_separator'] == 'nl' ? "\n" : $this->configuration['property_separator'];
-        $result[$field_key] = \implode($separator, $result[$field_key]);
-
         // EXPERIMENTAL: Attach thumbnail image data.
         if ($this->configuration['show_thumbnail']) {
           if (isset($values['target_id'])) {
             $storage = $this->entityTypeManager->getStorage('entity_form_display');
             $display = $storage->load($field_items->getFieldDefinition()->getTargetEntityTypeId() . '.' . $field_items->getEntity()->bundle() . '.default');
             if ($image_field = $display->getComponent($field_item->getFieldDefinition()->getName())) {
+              /** @var \Drupal\file\Entity\File $image */
               $image = $fileManager->load($values['target_id']);
-
-              $image_style[$field_key]['#thumbnail'] = [
+              $thumbnail = [
                 '#theme' => 'image_style',
                 '#uri' => $image->getFileUri(),
                 '#style_name' => $image_field['settings']['preview_image_style'],
               ];
-              $result = \array_merge($result, $image_style);
             }
           }
         }
+
+        $separator = $this->configuration['property_separator'] == 'nl' ? "\n" : $this->configuration['property_separator'];
+        $properties = \implode($separator, $item_data);
+        if (isset($thumbnail)) {
+          $result[$field_key]['#thumbnail'] = $thumbnail;
+          $result[$field_key]['data'] = $properties;
+        }
+        else {
+          $result[$field_key] = $properties;
+        }
       }
     }
 
diff --git a/tests/src/Functional/DiffPluginFileTest.php b/tests/src/Functional/DiffPluginFileTest.php
index 40f40b4..1d5eeff 100644
--- a/tests/src/Functional/DiffPluginFileTest.php
+++ b/tests/src/Functional/DiffPluginFileTest.php
@@ -126,44 +126,13 @@ class DiffPluginFileTest extends DiffPluginTestBase {
   }
 
   /**
-   * Tests the Image plugin.
+   * Tests the Image plugin for singe value fields.
    *
    * @see \Drupal\diff\Plugin\diff\Field\ImageFieldBuilder
    */
-  public function testImagePlugin(): void {
+  public function testImagePluginSingleValue(): void {
     // Add image field to the article content type.
-    $image_field_name = 'field_image';
-    FieldStorageConfig::create([
-      'field_name' => $image_field_name,
-      'entity_type' => 'node',
-      'type' => 'image',
-      'settings' => [],
-      'cardinality' => 1,
-    ])->save();
-
-    $field_config = FieldConfig::create([
-      'field_name' => $image_field_name,
-      'label' => 'Image',
-      'entity_type' => 'node',
-      'bundle' => 'article',
-      'required' => FALSE,
-      'settings' => ['alt_field' => 1],
-    ]);
-    $field_config->save();
-
-    $this->formDisplay->load('node.article.default')
-      ->setComponent($image_field_name, [
-        'type' => 'image_image',
-        'settings' => [],
-      ])
-      ->save();
-
-    $this->viewDisplay->load('node.article.default')
-      ->setComponent($image_field_name, [
-        'type' => 'image',
-        'settings' => [],
-      ])
-      ->save();
+    $this->setUpImageField('field_image');
 
     // Create an article.
     $node = $this->drupalCreateNode([
@@ -296,4 +265,90 @@ class DiffPluginFileTest extends DiffPluginTestBase {
     $this->assertSession()->responseNotContains($image_url);
   }
 
+  /**
+   * Tests the Image plugin for multi value fields.
+   *
+   * @see \Drupal\diff\Plugin\diff\Field\ImageFieldBuilder
+   */
+  public function testImagePluginMultiValue(): void {
+    // Add image field to the article content type.
+    $this->setUpImageField('field_image', 10);
+
+    // Create an article.
+    $node = $this->drupalCreateNode([
+      'type' => 'article',
+      'title' => 'Test article',
+    ]);
+
+    // Upload an image to the article.
+    $test_files = $this->drupalGetTestFiles('image');
+    $edit = ['files[field_image_0][]' => $this->fileSystem->realpath($test_files['1']->uri)];
+    $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, 'Save');
+    $edit = [
+      'field_image[0][alt]' => 'Image alt',
+      'revision' => TRUE,
+    ];
+    $this->submitForm($edit, 'Save');
+    $node = $this->drupalGetNodeByTitle('Test article', TRUE);
+
+    // Upload new image to the article.
+    $edit = ['files[field_image_1][]' => $this->fileSystem->realpath($test_files['2']->uri)];
+    $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, 'Save');
+    $edit = [
+      'field_image[1][alt]' => 'Image two alt',
+      'revision' => TRUE,
+    ];
+    $this->submitForm($edit, 'Save');
+
+    $this->drupalGet('node/' . $node->id() . '/revisions');
+    $this->submitForm([], 'Compare selected revisions');
+    // Assert alt text of both images are visible, and only once.
+    $this->assertSession()->pageTextMatchesCount(1, 'Alt: Image alt');
+    $this->assertSession()->pageTextMatchesCount(1, 'Alt: Image two alt');
+  }
+
+  /**
+   * Sets image with on the "article".
+   *
+   * @param string $field_name
+   * @param int $cardinality
+   * @param string $label
+   *
+   * @return void
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  protected function setUpImageField(string $field_name, int $cardinality = 1, string $label = 'Image'): void {
+    FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'type' => 'image',
+      'settings' => [],
+      'cardinality' => $cardinality,
+    ])->save();
+
+    $field_config = FieldConfig::create([
+      'field_name' => $field_name,
+      'label' => $label,
+      'entity_type' => 'node',
+      'bundle' => 'article',
+      'required' => FALSE,
+      'settings' => ['alt_field' => 1],
+    ]);
+    $field_config->save();
+
+    $this->formDisplay->load('node.article.default')
+      ->setComponent($field_name, [
+        'type' => 'image_image',
+        'settings' => [],
+      ])
+      ->save();
+
+    $this->viewDisplay->load('node.article.default')
+      ->setComponent($field_name, [
+        'type' => 'image',
+        'settings' => [],
+      ])
+      ->save();
+  }
+
 }
-- 
GitLab


From 2c27a2c57fcbb5a7a0de7f4863369a450830dd33 Mon Sep 17 00:00:00 2001
From: Kirill Roskolii <kirill.roskolii@catalyst.net.nz>
Date: Tue, 25 Feb 2025 11:20:54 +1300
Subject: [PATCH 2/4] Code style

---
 tests/src/Functional/DiffPluginFileTest.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/src/Functional/DiffPluginFileTest.php b/tests/src/Functional/DiffPluginFileTest.php
index 1d5eeff..a5fd4c8 100644
--- a/tests/src/Functional/DiffPluginFileTest.php
+++ b/tests/src/Functional/DiffPluginFileTest.php
@@ -311,11 +311,11 @@ class DiffPluginFileTest extends DiffPluginTestBase {
    * Sets image with on the "article".
    *
    * @param string $field_name
+   *   Field name.
    * @param int $cardinality
+   *   Field cardinality.
    * @param string $label
-   *
-   * @return void
-   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   Field label.
    */
   protected function setUpImageField(string $field_name, int $cardinality = 1, string $label = 'Image'): void {
     FieldStorageConfig::create([
-- 
GitLab


From e503d511fb0dfb69cff1ead34a33be942e6665c6 Mon Sep 17 00:00:00 2001
From: Kirill Roskolii <kirill.roskolii@catalyst.net.nz>
Date: Tue, 25 Feb 2025 11:22:40 +1300
Subject: [PATCH 3/4] Format correctly

---
 tests/src/Functional/DiffPluginFileTest.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/src/Functional/DiffPluginFileTest.php b/tests/src/Functional/DiffPluginFileTest.php
index a5fd4c8..02da470 100644
--- a/tests/src/Functional/DiffPluginFileTest.php
+++ b/tests/src/Functional/DiffPluginFileTest.php
@@ -303,8 +303,8 @@ class DiffPluginFileTest extends DiffPluginTestBase {
     $this->drupalGet('node/' . $node->id() . '/revisions');
     $this->submitForm([], 'Compare selected revisions');
     // Assert alt text of both images are visible, and only once.
-    $this->assertSession()->pageTextMatchesCount(1, 'Alt: Image alt');
-    $this->assertSession()->pageTextMatchesCount(1, 'Alt: Image two alt');
+    $this->assertSession()->pageTextMatchesCount(1, '/Alt: Image alt/');
+    $this->assertSession()->pageTextMatchesCount(1, '/Alt: Image two alt/');
   }
 
   /**
-- 
GitLab


From 858a30fc344892bb21a0c908ef70743e3e9908e8 Mon Sep 17 00:00:00 2001
From: Kirill Roskolii <kirill.roskolii@catalyst.net.nz>
Date: Tue, 25 Feb 2025 11:39:41 +1300
Subject: [PATCH 4/4] Re-structure test

---
 tests/src/Functional/DiffPluginFileTest.php | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/tests/src/Functional/DiffPluginFileTest.php b/tests/src/Functional/DiffPluginFileTest.php
index 02da470..9c6d120 100644
--- a/tests/src/Functional/DiffPluginFileTest.php
+++ b/tests/src/Functional/DiffPluginFileTest.php
@@ -280,7 +280,7 @@ class DiffPluginFileTest extends DiffPluginTestBase {
       'title' => 'Test article',
     ]);
 
-    // Upload an image to the article.
+    // Upload first image to the article.
     $test_files = $this->drupalGetTestFiles('image');
     $edit = ['files[field_image_0][]' => $this->fileSystem->realpath($test_files['1']->uri)];
     $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, 'Save');
@@ -289,7 +289,6 @@ class DiffPluginFileTest extends DiffPluginTestBase {
       'revision' => TRUE,
     ];
     $this->submitForm($edit, 'Save');
-    $node = $this->drupalGetNodeByTitle('Test article', TRUE);
 
     // Upload new image to the article.
     $edit = ['files[field_image_1][]' => $this->fileSystem->realpath($test_files['2']->uri)];
@@ -302,8 +301,10 @@ class DiffPluginFileTest extends DiffPluginTestBase {
 
     $this->drupalGet('node/' . $node->id() . '/revisions');
     $this->submitForm([], 'Compare selected revisions');
-    // Assert alt text of both images are visible, and only once.
-    $this->assertSession()->pageTextMatchesCount(1, '/Alt: Image alt/');
+    // Assert alt text of both images are visible. First image added in previous
+    // revision would have its alt text visible twice on both sides as context.
+    // Alt text of the second image should only be present once on right.
+    $this->assertSession()->pageTextMatchesCount(2, '/Alt: Image alt/');
     $this->assertSession()->pageTextMatchesCount(1, '/Alt: Image two alt/');
   }
 
-- 
GitLab