Commit 17419a9cf24 for woocommerce

commit 17419a9cf241a7a5de0abe9cb9da2a3d520c73e7
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date:   Mon Jul 13 08:49:56 2026 +0200

    Fix Product Gallery custom aspect ratios (#66441)

    * Fix Product Gallery image aspect ratios

    * Add changelog for Product Gallery aspect ratios

    * Fix Product Gallery aspect ratio CI checks

    * Strengthen Product Gallery aspect ratio tests

    * Clean up Product Gallery aspect ratio editor code

    * Reduce Product Gallery aspect ratio duplication

    * Tidy Product Gallery aspect ratio style handling

    * Scope Product Gallery ratio to large image

    * Remove Product Image utility extraction

    * Remove redundant Product Gallery aspect ratio styles

diff --git a/plugins/woocommerce/changelog/fix-product-gallery-custom-aspect-ratios b/plugins/woocommerce/changelog/fix-product-gallery-custom-aspect-ratios
new file mode 100644
index 00000000000..8598ec01cee
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-gallery-custom-aspect-ratios
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make Product Gallery layout honor custom Product Image aspect ratios.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/edit.tsx
index c8ab29c5d12..e997076e38e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/edit.tsx
@@ -4,11 +4,18 @@
 import {
 	InnerBlocks,
 	InspectorControls,
+	store as blockEditorStore,
 	useBlockProps,
 } from '@wordpress/block-editor';
-import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
+import {
+	BlockEditProps,
+	InnerBlockTemplate,
+	type BlockInstance,
+} from '@wordpress/blocks';
+import { useSelect } from '@wordpress/data';
 import { withProductDataContext } from '@woocommerce/shared-hocs';
 import { useProductDataContext } from '@woocommerce/shared-context';
+import { getSetting } from '@woocommerce/settings';
 import clsx from 'clsx';

 /**
@@ -16,6 +23,8 @@ import clsx from 'clsx';
  */
 import { ProductGalleryBlockSettings } from './block-settings/index';
 import type { ProductGalleryBlockAttributes } from './types';
+import { resolveAspectRatio } from '../../atomic/blocks/product-elements/image/utils';
+import type { BlockAttributes as ProductImageBlockAttributes } from '../../atomic/blocks/product-elements/image/types';

 const TEMPLATE: InnerBlockTemplate[] = [
 	[ 'woocommerce/product-gallery-thumbnails' ],
@@ -41,8 +50,88 @@ const TEMPLATE: InnerBlockTemplate[] = [
 	],
 ];

+const PRODUCT_IMAGE_BLOCK_NAME = 'woocommerce/product-image';
+
+type ParsedAspectRatio = {
+	width: string;
+	height: string;
+};
+
+const DEFAULT_ASPECT_RATIO: ParsedAspectRatio = { width: '1', height: '1' };
+
+type ProductImageBlock = BlockInstance<
+	Partial< ProductImageBlockAttributes >
+>;
+
+const isProductImageBlock = (
+	block: BlockInstance
+): block is ProductImageBlock => block.name === PRODUCT_IMAGE_BLOCK_NAME;
+
+const findProductImageBlock = (
+	blocks: BlockInstance[]
+): ProductImageBlock | undefined => {
+	for ( const block of blocks ) {
+		if ( isProductImageBlock( block ) ) {
+			return block;
+		}
+
+		const innerProductImageBlock = findProductImageBlock(
+			block.innerBlocks
+		);
+		if ( innerProductImageBlock ) {
+			return innerProductImageBlock;
+		}
+	}
+
+	return undefined;
+};
+
+const resolveProductImageAspectRatio = (
+	attributes: Partial< ProductImageBlockAttributes > | undefined,
+	storeAspectRatio: string | null
+): string | undefined => {
+	const { style, aspectRatio, imageSizing } = attributes ?? {};
+
+	return resolveAspectRatio(
+		style,
+		aspectRatio,
+		storeAspectRatio,
+		imageSizing
+	);
+};
+
+const parseAspectRatio = (
+	aspectRatio: string | undefined
+): ParsedAspectRatio => {
+	if ( ! aspectRatio ) {
+		return DEFAULT_ASPECT_RATIO;
+	}
+
+	const ratioParts = aspectRatio
+		.split( '/' )
+		.map( ( part ) => part.trim() )
+		.filter( Boolean );
+
+	if ( ratioParts.length === 0 || ratioParts.length > 2 ) {
+		return DEFAULT_ASPECT_RATIO;
+	}
+
+	const width = Number( ratioParts[ 0 ] );
+	const height = Number( ratioParts[ 1 ] ?? ratioParts[ 0 ] );
+
+	if ( ! Number.isFinite( width ) || ! Number.isFinite( height ) ) {
+		return DEFAULT_ASPECT_RATIO;
+	}
+
+	return {
+		width: String( width ),
+		height: String( height ),
+	};
+};
+
 export const Edit = withProductDataContext(
 	( {
+		clientId,
 		attributes,
 		setAttributes,
 		context,
@@ -56,11 +145,37 @@ export const Edit = withProductDataContext(
 		const hasProductContext = Boolean( context?.postId && product?.id );
 		const hasOneOrNoImages =
 			hasProductContext && ! isLoading && productImages.length <= 1;
+		const storeAspectRatio = getSetting< string | null >(
+			'thumbnailAspectRatio',
+			null
+		);
+		const productImageAspectRatio = useSelect(
+			( select ) => {
+				const productImageBlock = findProductImageBlock(
+					select( blockEditorStore ).getBlocks( clientId )
+				);
+
+				return resolveProductImageAspectRatio(
+					productImageBlock?.attributes,
+					storeAspectRatio
+				);
+			},
+			[ clientId, storeAspectRatio ]
+		);
+		const productGalleryAspectRatio = parseAspectRatio(
+			productImageAspectRatio
+		);
 		const blockProps = useBlockProps( {
 			className: clsx( 'wc-block-product-gallery', {
 				'wc-block-product-gallery--has-one-or-no-images':
 					hasOneOrNoImages,
 			} ),
+			style: {
+				'--wc-block-product-gallery-large-image-ratio-width':
+					productGalleryAspectRatio.width,
+				'--wc-block-product-gallery-large-image-ratio-height':
+					productGalleryAspectRatio.height,
+			},
 		} );

 		return (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/editor.scss
index c2b0585f989..7f28d2a89ca 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/editor.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/editor.scss
@@ -8,35 +8,8 @@
 	}
 }

-
-// In Editor there's different HTML structure than in the frontend
-// due to usage of inner blocks template (we cannot have wrapper elements)
-// hence we need to use manually assign aspect ratios that are otherwise handled
-// in the frontend.
+// In the editor there is no frontend wrapper, so the gallery ratio is inherited
+// from the parent Product Gallery block CSS variables.
 :where(.wc-block-product-gallery-large-image) {
-	&:has(.wc-block-components-product-image--aspect-ratio-auto),
-	&:has(.wc-block-components-product-image--aspect-ratio-1) {
-		aspect-ratio: 1/1;
-	}
-
-	&:has(.wc-block-components-product-image--aspect-ratio-4-3) {
-		aspect-ratio: 4/3;
-	}
-
-	&:has(.wc-block-components-product-image--aspect-ratio-3-4) {
-		aspect-ratio: 3/4;
-	}
-
-	&:has(.wc-block-components-product-image--aspect-ratio-3-2) {
-		aspect-ratio: 3/2;
-	}
-	&:has(.wc-block-components-product-image--aspect-ratio-2-3) {
-		aspect-ratio: 2/3;
-	}
-	&:has(.wc-block-components-product-image--aspect-ratio-16-9) {
-		aspect-ratio: 16/9;
-	}
-	&:has(.wc-block-components-product-image--aspect-ratio-9-16) {
-		aspect-ratio: 9/16;
-	}
+	aspect-ratio: var(--wc-block-product-gallery-large-image-ratio-width, 1) / var(--wc-block-product-gallery-large-image-ratio-height, 1);
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/style.scss
index b54d339bde1..a6b3c7335c4 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/style.scss
@@ -3,18 +3,17 @@ $thumbnails-gradient-size: 20%;
 $thumbnails-gap: 2%;
 $dialog-padding: 20px;

+:where(.wc-block-product-gallery) {
+	--wc-block-product-gallery-large-image-ratio-width: 1;
+	--wc-block-product-gallery-large-image-ratio-height: 1;
+}
+
 :where(.wc-block-product-gallery-large-image) {
 	width: 100%;
 	position: relative;
 	flex-grow: 1;
 	overflow: hidden;

-	//When Product Image aspect ratio is auto, we are setting constraints
-	//on the viewer container and display full images in square container.
-	&:has(.wc-block-components-product-image--aspect-ratio-auto) {
-		aspect-ratio: 1/1;
-	}
-
 	:where(.wc-block-product-gallery-large-image__container) {
 		display: flex;
 		overflow: hidden;
@@ -33,10 +32,7 @@ $dialog-padding: 20px;
 		display: flex;
 		align-items: center;
 		justify-content: center;
-
-		&:has(.wc-block-components-product-image--aspect-ratio-auto) {
-			aspect-ratio: 1/1;
-		}
+		aspect-ratio: var(--wc-block-product-gallery-large-image-ratio-width, 1) / var(--wc-block-product-gallery-large-image-ratio-height, 1);
 	}

 	.wc-block-product-gallery-large-image__wrapper[hidden] {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/test/block.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/test/block.ts
index dad090075de..9d59f3084ac 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/test/block.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-gallery/test/block.ts
@@ -203,8 +203,14 @@ describe( 'Product Gallery Block', () => {
 		expect( productImage ).toHaveAttribute( 'alt', 'Test 1' );
 	} );

-	it( 'should ensure thumbnail height matches viewer height with custom aspect ratio', async () => {
-		await setup( { aspectRatio: '16/9' } );
+	it( 'should expose custom product image ratios as large image CSS variables', async () => {
+		await setup( {
+			style: {
+				dimensions: {
+					aspectRatio: '3/5',
+				},
+			},
+		} );

 		// Get the viewer
 		const productImage = screen.getByTestId( 'product-image' );
@@ -215,22 +221,21 @@ describe( 'Product Gallery Block', () => {
 			'.wc-block-components-product-image'
 		);
 		expect( imageContainer ).toHaveClass(
-			'wc-block-components-product-image--aspect-ratio-16-9'
+			'wc-block-components-product-image--aspect-ratio-3-5'
 		);
+		const productGalleryBlock = screen.getByRole( 'document', {
+			name: /Block: Product Gallery/i,
+		} );
+		expect( productGalleryBlock ).toHaveStyle( {
+			'--wc-block-product-gallery-large-image-ratio-width': '3',
+			'--wc-block-product-gallery-large-image-ratio-height': '5',
+		} );

-		// Get the thumbnails block
 		const thumbnailsBlock = screen.getByRole( 'document', {
 			name: /Block: Thumbnails/i,
 		} );
 		expect( thumbnailsBlock ).toBeInTheDocument();

-		// Get the heights
-		const viewerHeight = productImage.getBoundingClientRect().height;
-		const thumbnailHeight = thumbnailsBlock.getBoundingClientRect().height;
-
-		// Check that the heights match
-		expect( thumbnailHeight ).toBe( viewerHeight );
-
 		// wp-6.8: upstream @wordpress/* deprecation warnings that we cannot
 		// opt out of without changing the visual output.
 		expect( console ).toHaveWarned();
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 8ab124ee289..4ea2973c9c5 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -52437,24 +52437,12 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/ProductFilters.php

-		-
-			message: '#^Access to property \$context on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Blocks/BlockTypes/ProductGallery.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductGallery\:\:render_dialog\(\) should return string but returns string\|false\.$#'
 			identifier: return.type
 			count: 1
 			path: src/Blocks/BlockTypes/ProductGallery.php

-		-
-			message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductGallery\:\:render\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Blocks/BlockTypes/ProductGallery.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductGalleryLargeImage\:\:enqueue_assets\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php
index c8711f6233e..dc0133ffb9d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php
@@ -112,12 +112,138 @@ class ProductGallery extends AbstractBlock {
 		return $gallery_html;
 	}

+	/**
+	 * Find the Product Image block attributes used by the gallery.
+	 *
+	 * @param iterable<\WP_Block> $inner_blocks Inner blocks to search.
+	 * @return array|null
+	 */
+	private function get_product_image_attributes( $inner_blocks ) {
+		foreach ( $inner_blocks as $inner_block ) {
+			if ( 'woocommerce/product-image' === $inner_block->name ) {
+				return $inner_block->parsed_block['attrs'] ?? array();
+			}
+
+			$found_attributes = $this->get_product_image_attributes( $inner_block->inner_blocks );
+			if ( null !== $found_attributes ) {
+				return $found_attributes;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get the store thumbnail aspect ratio from WooCommerce Customizer settings.
+	 *
+	 * @return string|null CSS aspect ratio value, or null when uncropped.
+	 */
+	private function get_store_thumbnail_aspect_ratio() {
+		$cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' );
+
+		if ( 'uncropped' === $cropping ) {
+			return null;
+		}
+
+		if ( 'custom' === $cropping ) {
+			$width  = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) );
+			$height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) );
+
+			return $width . '/' . $height;
+		}
+
+		return str_replace( ':', '/', $cropping );
+	}
+
+	/**
+	 * Resolve the gallery aspect ratio from the nested Product Image block.
+	 *
+	 * @param \WP_Block $block The Product Gallery block.
+	 * @return string|null CSS aspect ratio value, or null when no ratio should be applied.
+	 */
+	private function resolve_product_image_aspect_ratio( $block ) {
+		$attributes = $this->get_product_image_attributes( $block->inner_blocks );
+
+		if ( empty( $attributes ) ) {
+			return null;
+		}
+
+		if ( ! empty( $attributes['style']['dimensions']['aspectRatio'] ) ) {
+			return $attributes['style']['dimensions']['aspectRatio'];
+		}
+
+		if ( ! empty( $attributes['aspectRatio'] ) ) {
+			return $attributes['aspectRatio'];
+		}
+
+		$image_sizing = $attributes['imageSizing'] ?? 'single';
+
+		if ( 'thumbnail' === $image_sizing || 'cropped' === $image_sizing ) {
+			return $this->get_store_thumbnail_aspect_ratio();
+		}
+
+		return null;
+	}
+
+	/**
+	 * Parse an aspect ratio into CSS variable values.
+	 *
+	 * @param string|null $aspect_ratio CSS aspect ratio value.
+	 * @return array
+	 */
+	private function parse_aspect_ratio( $aspect_ratio ) {
+		$default_aspect_ratio = array(
+			'width'  => '1',
+			'height' => '1',
+		);
+
+		if ( empty( $aspect_ratio ) || ! is_string( $aspect_ratio ) ) {
+			return $default_aspect_ratio;
+		}
+
+		$parts = array_map( 'trim', explode( '/', $aspect_ratio ) );
+		if ( count( $parts ) > 2 ) {
+			return $default_aspect_ratio;
+		}
+
+		$width  = (float) $parts[0];
+		$height = isset( $parts[1] ) ? (float) $parts[1] : $width;
+
+		if ( $width <= 0 || $height <= 0 ) {
+			return $default_aspect_ratio;
+		}
+
+		return array(
+			'width'  => (string) $width,
+			'height' => (string) $height,
+		);
+	}
+
+	/**
+	 * Get the gallery aspect ratio CSS variables.
+	 *
+	 * @param \WP_Block $block The Product Gallery block.
+	 * @return string
+	 */
+	private function get_aspect_ratio_style( $block ) {
+		$aspect_ratio = $this->parse_aspect_ratio(
+			$this->resolve_product_image_aspect_ratio( $block )
+		);
+
+		return sprintf(
+			'--wc-block-product-gallery-large-image-ratio-width:%1$s;' .
+			'--wc-block-product-gallery-large-image-ratio-height:%2$s;',
+			$aspect_ratio['width'],
+			$aspect_ratio['height']
+		);
+	}
+
 	/**
 	 * Include and render the block.
 	 *
-	 * @param array    $attributes Block attributes. Default empty array.
-	 * @param string   $content    Block content. Default empty string.
-	 * @param WP_Block $block      Block instance.
+	 * @param array     $attributes Block attributes. Default empty array.
+	 * @param string    $content    Block content. Default empty string.
+	 * @param \WP_Block $block     Block instance.
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
@@ -199,6 +325,15 @@ class ProductGallery extends AbstractBlock {

 			$p->add_class( $classname );
 			$p->add_class( $classname_single_image );
+
+			$style = $p->get_attribute( 'style' );
+			$style = is_string( $style ) ? trim( $style ) : '';
+
+			if ( '' !== $style && ';' !== substr( $style, -1 ) ) {
+				$style .= ';';
+			}
+
+			$p->set_attribute( 'style', $style . $this->get_aspect_ratio_style( $block ) );
 			$html = $p->get_updated_html();
 		}

diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductGallery.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductGallery.php
index e88f16fa246..c7c1a711fb8 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductGallery.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductGallery.php
@@ -11,6 +11,45 @@ use WC_Helper_Product;
  */
 class ProductGallery extends \WP_UnitTestCase {

+	/**
+	 * Previous thumbnail cropping option values to restore after tests.
+	 *
+	 * @var array<string, mixed>
+	 */
+	private $prev_thumbnail_cropping_options = array();
+
+	/**
+	 * Set up.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$option_names = array(
+			'woocommerce_thumbnail_cropping',
+			'woocommerce_thumbnail_cropping_custom_width',
+			'woocommerce_thumbnail_cropping_custom_height',
+		);
+
+		foreach ( $option_names as $option_name ) {
+			$this->prev_thumbnail_cropping_options[ $option_name ] = get_option( $option_name, null );
+		}
+	}
+
+	/**
+	 * Tear down.
+	 */
+	public function tearDown(): void {
+		foreach ( $this->prev_thumbnail_cropping_options as $option_name => $value ) {
+			if ( null === $value ) {
+				delete_option( $option_name );
+			} else {
+				update_option( $option_name, $value );
+			}
+		}
+
+		parent::tearDown();
+	}
+
 	/**
 	 * Helper method to create a product with multiple images.
 	 *
@@ -56,9 +95,25 @@ class ProductGallery extends \WP_UnitTestCase {
 	 *
 	 * @param int    $product_id The product ID.
 	 * @param string $gallery_attributes Optional gallery attributes.
+	 * @param array  $product_image_attributes Optional Product Image block attributes.
 	 * @return string The rendered markup.
 	 */
-	private function render_product_gallery( $product_id, $gallery_attributes = '' ) {
+	private function render_product_gallery(
+		$product_id,
+		$gallery_attributes = '',
+		$product_image_attributes = array()
+	) {
+		$product_image_attributes = wp_json_encode(
+			array_merge(
+				array(
+					'showProductLink'                  => false,
+					'showSaleBadge'                    => false,
+					'isDescendentOfSingleProductBlock' => true,
+				),
+				$product_image_attributes
+			)
+		);
+
 		return do_blocks(
 			sprintf(
 				'<!-- wp:woocommerce/single-product {"productId":%d} -->
@@ -69,7 +124,7 @@ class ProductGallery extends \WP_UnitTestCase {

 						<!-- wp:woocommerce/product-gallery-large-image -->
 						<div class="wp-block-woocommerce-product-gallery-large-image wc-block-product-gallery-large-image__inner-blocks">
-							<!-- wp:woocommerce/product-image {"showProductLink":false,"showSaleBadge":false,"isDescendentOfSingleProductBlock":true} /-->
+							<!-- wp:woocommerce/product-image %s /-->

 							<!-- wp:woocommerce/product-sale-badge {"align":"right"} /-->

@@ -83,7 +138,8 @@ class ProductGallery extends \WP_UnitTestCase {
 				</div>
 				<!-- /wp:woocommerce/single-product -->',
 				$product_id,
-				$gallery_attributes
+				$gallery_attributes,
+				$product_image_attributes
 			)
 		);
 	}
@@ -105,6 +161,26 @@ class ProductGallery extends \WP_UnitTestCase {
 		}
 	}

+	/**
+	 * Assert that rendered gallery markup contains the expected large image aspect ratio CSS variables.
+	 *
+	 * @param string $markup Rendered gallery markup.
+	 * @param string $width Expected ratio width.
+	 * @param string $height Expected ratio height.
+	 */
+	private function assert_product_gallery_large_image_ratio_variables( string $markup, string $width, string $height ): void {
+		$this->assertStringContainsString(
+			"--wc-block-product-gallery-large-image-ratio-width:{$width};",
+			$markup,
+			"Expected Product Gallery markup to expose a {$width}:{$height} large image aspect ratio width variable."
+		);
+		$this->assertStringContainsString(
+			"--wc-block-product-gallery-large-image-ratio-height:{$height};",
+			$markup,
+			"Expected Product Gallery markup to expose a {$width}:{$height} large image aspect ratio height variable."
+		);
+	}
+
 	/**
 	 * Test that the ProductGallery block renders correctly with multiple images.
 	 */
@@ -134,6 +210,86 @@ class ProductGallery extends \WP_UnitTestCase {
 		$this->cleanup_product_data( $data );
 	}

+	/**
+	 * @testdox Should expose Product Image style aspect ratios as large image CSS variables.
+	 */
+	public function test_product_gallery_exposes_custom_product_image_aspect_ratio_css_variables(): void {
+		$data = $this->create_product_with_gallery( 3 );
+
+		$markup = $this->render_product_gallery(
+			$data['product']->get_id(),
+			'',
+			array(
+				'style' => array(
+					'dimensions' => array(
+						'aspectRatio' => '3/5',
+					),
+				),
+			)
+		);
+
+		$this->assert_product_gallery_large_image_ratio_variables( $markup, '3', '5' );
+
+		$this->cleanup_product_data( $data );
+	}
+
+	/**
+	 * @testdox Should expose Product Image aspectRatio attributes as large image CSS variables.
+	 */
+	public function test_product_gallery_exposes_legacy_product_image_aspect_ratio_css_variables(): void {
+		$data = $this->create_product_with_gallery( 3 );
+
+		$markup = $this->render_product_gallery(
+			$data['product']->get_id(),
+			'',
+			array(
+				'aspectRatio' => '7/4',
+			)
+		);
+
+		$this->assert_product_gallery_large_image_ratio_variables( $markup, '7', '4' );
+
+		$this->cleanup_product_data( $data );
+	}
+
+	/**
+	 * @testdox Should use custom store thumbnail ratios for Product Image thumbnail sizing.
+	 * @dataProvider product_image_thumbnail_sizing_data
+	 *
+	 * @param string $image_sizing Product Image sizing mode.
+	 */
+	public function test_product_gallery_uses_store_thumbnail_ratio_for_thumbnail_product_image_sizing( string $image_sizing ): void {
+		$data = $this->create_product_with_gallery( 3 );
+
+		update_option( 'woocommerce_thumbnail_cropping', 'custom' );
+		update_option( 'woocommerce_thumbnail_cropping_custom_width', '5' );
+		update_option( 'woocommerce_thumbnail_cropping_custom_height', '8' );
+
+		$markup = $this->render_product_gallery(
+			$data['product']->get_id(),
+			'',
+			array(
+				'imageSizing' => $image_sizing,
+			)
+		);
+
+		$this->assert_product_gallery_large_image_ratio_variables( $markup, '5', '8' );
+
+		$this->cleanup_product_data( $data );
+	}
+
+	/**
+	 * Data provider for Product Image thumbnail sizing modes.
+	 *
+	 * @return array[]
+	 */
+	public function product_image_thumbnail_sizing_data(): array {
+		return array(
+			'thumbnail sizing' => array( 'thumbnail' ),
+			'cropped sizing'   => array( 'cropped' ),
+		);
+	}
+
 	/**
 	 * Test that the ProductGallery block renders correctly with hover zoom enabled.
 	 */