Commit b8d1046c86 for woocommerce
commit b8d1046c8637c2532e3e1ca95bbc57413ae96ff4
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon Jun 30 09:24:17 2025 +0200
Cherry pick #59157 into `trunk` (#59191)
Product Gallery: fix large image that may be displaying single image all over for variable products (#59157)
* Check if image belong to the product among all of the images including variations
* Add unit tests
* Add changelog
* Test fixes
* Fix unit tests
* Cleanup tests by extracting product creation to helpers
* Fix lint
* Fix lint again
Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/fix-product-gallery-variable-products b/plugins/woocommerce/changelog/fix-product-gallery-variable-products
new file mode 100644
index 0000000000..7df27979ff
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-gallery-variable-products
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Product Gallery: fix large image that may be displaying single image all over for variable products
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
index 09150c2d23..fe6113c491 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
+use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
@@ -159,7 +160,7 @@ class ProductImage extends AbstractBlock {
}
$featured_image_id = (int) $product->get_image_id();
- $gallery_image_ids = $product->get_gallery_image_ids();
+ $gallery_image_ids = ProductGalleryUtils::get_all_image_ids( $product );
$available_image_ids = array_merge( [ $featured_image_id ], $gallery_image_ids );
$provided_image_id_is_valid = $image_id && in_array( $image_id, $available_image_ids, true );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
new file mode 100644
index 0000000000..19be72a41b
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
@@ -0,0 +1,245 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+
+/**
+ * Tests for the ProductImage block type
+ */
+class ProductImage extends \WP_UnitTestCase {
+
+ /**
+ * Helper method to create a simple product with an image.
+ *
+ * @param string $image_title Optional title for the image.
+ * @return array Array containing 'product' and 'image_id'.
+ */
+ private function create_product_with_image( $image_title = 'Test Product Image' ) {
+ $product = WC_Helper_Product::create_simple_product();
+
+ $image_id = wp_insert_attachment(
+ array(
+ 'post_title' => $image_title,
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image/jpeg',
+ )
+ );
+ $product->set_image_id( $image_id );
+ $product->save();
+
+ return array(
+ 'product' => $product,
+ 'image_id' => $image_id,
+ );
+ }
+
+ /**
+ * Helper method to create a variable product with main image, gallery images, and variation images.
+ *
+ * @param int $gallery_count Number of gallery images to create.
+ * @param int $variation_count Number of variations to create images for.
+ * @return array Array containing 'product', 'main_image_id', 'gallery_image_ids', and 'variation_image_ids'.
+ */
+ private function create_variable_product_with_images( $gallery_count = 2, $variation_count = 1 ) {
+ $variable_product = WC_Helper_Product::create_variation_product();
+
+ // Create and set the main product image.
+ $main_image_id = wp_insert_attachment(
+ array(
+ 'post_title' => 'Main Product Image',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image/jpeg',
+ )
+ );
+ $variable_product->set_image_id( $main_image_id );
+
+ // Create gallery images.
+ $gallery_image_ids = array();
+ for ( $i = 0; $i < $gallery_count; $i++ ) {
+ $gallery_image_ids[] = wp_insert_attachment(
+ array(
+ 'post_title' => 'Gallery Image ' . ( $i + 1 ),
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image/jpeg',
+ )
+ );
+ }
+ $variable_product->set_gallery_image_ids( $gallery_image_ids );
+ $variable_product->save();
+
+ // Create variation images.
+ $variation_image_ids = array();
+ $variations = $variable_product->get_children();
+ $variations_count = min( $variation_count, count( $variations ) );
+
+ for ( $i = 0; $i < $variations_count; $i++ ) {
+ $variation_image_id = wp_insert_attachment(
+ array(
+ 'post_title' => 'Variation Image ' . ( $i + 1 ),
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image/jpeg',
+ )
+ );
+
+ $variation = wc_get_product( $variations[ $i ] );
+ $variation->set_image_id( $variation_image_id );
+ $variation->save();
+
+ $variation_image_ids[] = $variation_image_id;
+ }
+
+ return array(
+ 'product' => $variable_product,
+ 'main_image_id' => $main_image_id,
+ 'gallery_image_ids' => $gallery_image_ids,
+ 'variation_image_ids' => $variation_image_ids,
+ );
+ }
+
+ /**
+ * Test that the ProductImage block renders correctly for a simple product.
+ */
+ public function test_product_image_render_simple_product() {
+ $data = $this->create_product_with_image();
+
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup );
+ $this->assertStringContainsString( 'data-testid="product-image"', $markup );
+ $this->assertStringContainsString( 'data-image-id="' . $data['image_id'] . '"', $markup );
+
+ // Clean up.
+ $data['product']->delete( true );
+ wp_delete_attachment( $data['image_id'], true );
+ }
+
+ /**
+ * Test that the ProductImage block renders correctly for a variable product with variation images.
+ * This is the main test case: if product is variable product and has some images attached to the variation
+ * (but not in the main gallery) and the imageId of variation image is provided via context,
+ * it still recognises the imageId as its own image.
+ */
+ public function test_product_image_render_variable_product_with_variation_images() {
+ $data = $this->create_variable_product_with_images( 2, 1 );
+ $variation_image_id = $data['variation_image_ids'][0];
+
+ // Test that the ProductImage block recognizes the variation image when provided via context.
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageId":' . $variation_image_id . '} /--><!-- /wp:woocommerce/single-product -->' );
+
+ // The block should recognize the variation image as valid and use it.
+ $this->assertStringContainsString( 'data-image-id="' . $variation_image_id . '"', $markup );
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup );
+
+ // Test that the block falls back to the main product image when no imageId is provided.
+ $markup_no_image_id = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+ $this->assertStringContainsString( 'data-image-id="' . $data['main_image_id'] . '"', $markup_no_image_id );
+
+ // Test that the block rejects invalid image IDs.
+ $invalid_image_id = 99999;
+ $markup_invalid = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageId":' . $invalid_image_id . '} /--><!-- /wp:woocommerce/single-product -->' );
+ // Should fall back to main product image when invalid image ID is provided.
+ $this->assertStringContainsString( 'data-image-id="' . $data['main_image_id'] . '"', $markup_invalid );
+
+ // Clean up.
+ $data['product']->delete( true );
+ wp_delete_attachment( $data['main_image_id'], true );
+ wp_delete_attachment( $variation_image_id, true );
+ foreach ( $data['gallery_image_ids'] as $gallery_image_id ) {
+ wp_delete_attachment( $gallery_image_id, true );
+ }
+ }
+
+ /**
+ * Test that the ProductImage block renders correctly with different image sizing options.
+ */
+ public function test_product_image_render_with_different_sizing() {
+ $data = $this->create_product_with_image();
+
+ // Test with 'single' image sizing.
+ $markup_single = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageSizing":"single"} /--><!-- /wp:woocommerce/single-product -->' );
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup_single );
+
+ // Test with 'thumbnail' image sizing.
+ $markup_thumbnail = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"imageSizing":"thumbnail"} /--><!-- /wp:woocommerce/single-product -->' );
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup_thumbnail );
+
+ // Clean up.
+ $data['product']->delete( true );
+ wp_delete_attachment( $data['image_id'], true );
+ }
+
+ /**
+ * Test that the ProductImage block renders correctly with sale badge.
+ */
+ public function test_product_image_render_with_sale_badge() {
+ $data = $this->create_product_with_image();
+ $data['product']->set_regular_price( 10 );
+ $data['product']->set_sale_price( 5 );
+ $data['product']->save();
+
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"showSaleBadge":true} /--><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup );
+ $this->assertStringContainsString( 'wp-block-woocommerce-product-sale-badge', $markup );
+
+ // Clean up.
+ $data['product']->delete( true );
+ wp_delete_attachment( $data['image_id'], true );
+ }
+
+ /**
+ * Test that the ProductImage block renders correctly with inner blocks content.
+ */
+ public function test_product_image_render_with_inner_blocks() {
+ $data = $this->create_product_with_image();
+
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image --><div class="custom-inner-block">Custom content</div><!-- /wp:woocommerce/product-image --><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup );
+ $this->assertStringContainsString( 'wc-block-components-product-image__inner-container', $markup );
+ $this->assertStringContainsString( 'custom-inner-block', $markup );
+ $this->assertStringContainsString( 'Custom content', $markup );
+
+ // Clean up.
+ $data['product']->delete( true );
+ wp_delete_attachment( $data['image_id'], true );
+ }
+
+ /**
+ * Test that the ProductImage block handles products without images correctly.
+ */
+ public function test_product_image_render_without_images() {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->save();
+
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product->get_id() . '} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup );
+ // Should contain placeholder image.
+ $this->assertStringContainsString( 'woocommerce-placeholder', $markup );
+
+ // Clean up.
+ $product->delete( true );
+ }
+
+ /**
+ * Test that the ProductImage block handles invalid product IDs correctly.
+ */
+ public function test_product_image_render_with_invalid_product() {
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":99999} --><!-- wp:woocommerce/product-image /--><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertEmpty( $markup );
+ }
+
+ /**
+ * Test that the ProductImage block handles missing postId context correctly.
+ */
+ public function test_product_image_render_without_post_id() {
+ $markup = do_blocks( '<!-- wp:woocommerce/product-image --><!-- /wp:woocommerce/product-image -->' );
+
+ $this->assertEmpty( $markup );
+ }
+}