Commit 3efc8b28d99 for woocommerce

commit 3efc8b28d997b5bd5e1cb5809bda4eb2a076402a
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Wed Apr 29 16:54:25 2026 +0200

    Add wc-visual attribute type to allow setting colors for attributes (#64324)

    * Add wc-visual attribute type to allow setting colors for attributes

    * Add changelog

    * PHPStan

    * Update test

    * Remove unnecessary formatting changes

    * Minor cleanups

    * Make sure test is correctly reset

    * Update comment

    * Change default attribute label from Select to Text

    * Add testdox annotation for visual attribute type registration test

    * Don't delete attribute color when updating term programatically

    * Fix attribute selector not showing up for wc-visual attributes

    * Lint

    * Add feature flag for wc-visual attribute type (#64344)

    * Add feature flag for wc-visual attribute type

    * Add changelog

    * Rename the feature flag to use singular

    * Linting

diff --git a/plugins/woocommerce/changelog/add-wc-visual-attribute-type-feature-flag b/plugins/woocommerce/changelog/add-wc-visual-attribute-type-feature-flag
new file mode 100644
index 00000000000..ad7d87dc717
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-wc-visual-attribute-type-feature-flag
@@ -0,0 +1,5 @@
+Significance: patch
+Type: add
+Comment: Add feature flag for wc-visual attribute type
+
+
diff --git a/plugins/woocommerce/changelog/fix-add-wc-visual-attribute-type b/plugins/woocommerce/changelog/fix-add-wc-visual-attribute-type
new file mode 100644
index 00000000000..4a0a34a049a
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-add-wc-visual-attribute-type
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add wc-visual attribute type to allow setting colors for attributes
diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json
index 83b2902f3b2..221e75c4bfa 100644
--- a/plugins/woocommerce/client/admin/config/core.json
+++ b/plugins/woocommerce/client/admin/config/core.json
@@ -39,6 +39,7 @@
 		"woo-mobile-welcome": true,
 		"wc-pay-promotion": true,
 		"wc-pay-welcome-page": true,
+		"wc-visual-attribute": false,
 		"async-product-editor-category-field": false,
 		"launch-your-store": true,
 		"product-editor-template-system": false,
diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json
index b14ae52bab7..4605c0171d3 100644
--- a/plugins/woocommerce/client/admin/config/development.json
+++ b/plugins/woocommerce/client/admin/config/development.json
@@ -39,6 +39,7 @@
 		"woo-mobile-welcome": true,
 		"wc-pay-promotion": true,
 		"wc-pay-welcome-page": true,
+		"wc-visual-attribute": true,
 		"async-product-editor-category-field": true,
 		"launch-your-store": true,
 		"product-editor-template-system": false,
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index 931440c62eb..aa08c5ff83d 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -9585,3 +9585,14 @@ body.woocommerce-settings-payments-section_legacy {
 		background: #f0f0f1;
 	}
 }
+
+.wc-admin-color-swatch {
+	display: inline-block;
+	vertical-align: middle;
+	vertical-align: center;
+	border: 1px solid #7e8993;
+	border-radius: 2px;
+	margin-right: 6px;
+	width: 12px;
+	height: 12px;
+}
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
index 2b5bf985b07..9157f2d5ca6 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
@@ -261,7 +261,7 @@ class WC_Admin_Attributes {
 									</td>
 								</tr>
 								<?php
-							}
+							}//end if
 							?>
 							<tr class="form-field form-required">
 								<th scope="row" valign="top">
@@ -283,7 +283,9 @@ class WC_Admin_Attributes {
 					<p class="submit"><button type="submit" name="save_attribute" id="submit" class="button-primary" value="<?php esc_attr_e( 'Update', 'woocommerce' ); ?>"><?php esc_html_e( 'Update', 'woocommerce' ); ?></button></p>
 					<?php wp_nonce_field( 'woocommerce-save-attribute_' . $edit ); ?>
 				</form>
-			<?php } ?>
+				<?php
+			}//end if
+			?>
 		</div>
 		<?php
 	}
@@ -392,10 +394,10 @@ class WC_Admin_Attributes {
 														} else {
 															/* translators: %s: Total count of terms available for the attribute */
 															echo esc_html( sprintf( __( '%s terms', 'woocommerce' ), $total_count ) );
-														}
+														}//end if
 													} else {
 															echo '<span class="na">&ndash;</span><br />';
-													}
+													}//end if
 													?>
 													<br /><a href="edit-tags.php?taxonomy=<?php echo esc_attr( wc_attribute_taxonomy_name( $tax->attribute_name ) ); ?>&amp;post_type=product" class="configure-terms"><?php esc_html_e( 'Configure terms', 'woocommerce' ); ?></a>
 												</td>
@@ -403,12 +405,13 @@ class WC_Admin_Attributes {
 											<?php
 										endforeach;
 								} else {
+									$column_count = wc_has_custom_attribute_types() ? '5' : '4';
 									?>
 										<tr>
-											<td colspan="6"><?php esc_html_e( 'No attributes currently exist.', 'woocommerce' ); ?></td>
+											<td colspan="<?php echo esc_attr( $column_count ); ?>"><?php esc_html_e( 'No attributes currently exist.', 'woocommerce' ); ?></td>
 										</tr>
 										<?php
-								}
+								}//end if
 								?>
 							</tbody>
 						</table>
@@ -469,7 +472,7 @@ class WC_Admin_Attributes {
 										<p class="description"><?php esc_html_e( "Determines how this attribute's values are displayed.", 'woocommerce' ); ?></p>
 									</div>
 									<?php
-								}
+								}//end if
 								?>

 								<div class="form-field">
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php b/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
index 16d02bd44fb..1ccbce52456 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
@@ -53,7 +53,7 @@ class WC_Admin_Taxonomies {
 		add_action( 'create_term', array( $this, 'create_term' ), 5, 3 );
 		add_action(
 			'delete_product_cat',
-			function() {
+			function () {
 				wc_get_container()->get( AssignDefaultCategory::class )->schedule_action();
 			}
 		);
@@ -80,15 +80,21 @@ class WC_Admin_Taxonomies {

 		if ( ! empty( $attribute_taxonomies ) ) {
 			foreach ( $attribute_taxonomies as $attribute ) {
-				add_action( 'pa_' . $attribute->attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) );
+				$taxonomy = 'pa_' . $attribute->attribute_name;
+				add_action( $taxonomy . '_pre_add_form', array( $this, 'product_attribute_description' ) );
+				add_action( $taxonomy . '_add_form_fields', array( $this, 'add_product_attribute_term_fields' ) );
+				add_action( $taxonomy . '_edit_form_fields', array( $this, 'edit_product_attribute_term_fields' ), 10, 1 );
+				add_filter( "manage_edit-{$taxonomy}_columns", array( $this, 'add_term_color_columns' ) );
+				add_filter( "manage_{$taxonomy}_custom_column", array( $this, 'render_term_color_column' ), 10, 3 );
 			}
 		}

 		// Maintain hierarchy of terms.
 		add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) );

-		// Admin footer scripts for this product categories admin screen.
+		// Admin footer scripts for taxonomy screens.
 		add_action( 'admin_footer', array( $this, 'scripts_at_product_cat_screen_footer' ) );
+		add_action( 'admin_footer', array( $this, 'scripts_at_visual_attribute_screen_footer' ) );
 	}

 	/**
@@ -305,6 +311,74 @@ class WC_Admin_Taxonomies {
 		<?php
 	}

+	/**
+	 * Check if the current taxonomy should show visual swatch controls.
+	 *
+	 * @param string $taxonomy Taxonomy slug.
+	 * @return bool
+	 *
+	 * @internal
+	 */
+	private function is_visual_product_attribute_taxonomy( $taxonomy ) {
+		if ( ! taxonomy_is_product_attribute( $taxonomy ) ) {
+			return false;
+		}
+
+		$attribute_slug = wc_attribute_taxonomy_slug( $taxonomy );
+
+		foreach ( wc_get_attribute_taxonomies() as $attribute_taxonomy ) {
+			if ( $attribute_slug === $attribute_taxonomy->attribute_name ) {
+				return 'wc-visual' === $attribute_taxonomy->attribute_type;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Add custom fields for product attribute terms.
+	 *
+	 * @param string $taxonomy Taxonomy slug.
+	 * @return void
+	 *
+	 * @internal
+	 */
+	public function add_product_attribute_term_fields( $taxonomy ) {
+		if ( ! $this->is_visual_product_attribute_taxonomy( $taxonomy ) ) {
+			return;
+		}
+		?>
+		<div class="form-field term-color-wrap">
+			<label for="term_color"><?php esc_html_e( 'Color value', 'woocommerce' ); ?></label>
+			<input name="term_color" id="term_color" type="color" value="" />
+		</div>
+		<?php
+	}
+
+	/**
+	 * Edit custom fields for product attribute terms.
+	 *
+	 * @param WP_Term $term Current term.
+	 * @return void
+	 *
+	 * @internal
+	 */
+	public function edit_product_attribute_term_fields( $term ) {
+		if ( ! $this->is_visual_product_attribute_taxonomy( $term->taxonomy ) ) {
+			return;
+		}
+
+		$color_value = get_term_meta( $term->term_id, 'color', true );
+		?>
+		<tr class="form-field term-color-wrap">
+			<th scope="row" valign="top"><label for="term_color"><?php esc_html_e( 'Color value', 'woocommerce' ); ?></label></th>
+			<td>
+				<input name="term_color" id="term_color" type="color" value="<?php echo esc_attr( $color_value ); ?>" />
+			</td>
+		</tr>
+		<?php
+	}
+
 	/**
 	 * Save category fields
 	 *
@@ -319,6 +393,13 @@ class WC_Admin_Taxonomies {
 		if ( isset( $_POST['product_cat_thumbnail_id'] ) && 'product_cat' === $taxonomy ) { // WPCS: CSRF ok, input var ok.
 			update_term_meta( $term_id, 'thumbnail_id', absint( $_POST['product_cat_thumbnail_id'] ) ); // WPCS: CSRF ok, input var ok.
 		}
+		if ( $this->is_visual_product_attribute_taxonomy( $taxonomy ) ) {
+			$color_value = isset( $_POST['term_color'] ) ? sanitize_hex_color( wp_unslash( $_POST['term_color'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+			if ( $color_value ) {
+				update_term_meta( $term_id, 'color', $color_value );
+			}
+		}
 	}

 	/**
@@ -364,6 +445,75 @@ class WC_Admin_Taxonomies {
 		);
 	}

+	/**
+	 * Add custom columns for product attribute terms.
+	 *
+	 * @param array $columns Existing columns.
+	 * @return array
+	 *
+	 * @internal
+	 */
+	public function add_term_color_columns( $columns ) {
+		$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( ! $this->is_visual_product_attribute_taxonomy( $taxonomy ) ) {
+			return $columns;
+		}
+
+		$new_columns = array();
+		foreach ( $columns as $key => $label ) {
+			if ( 'slug' === $key ) {
+				$new_columns['color'] = __( 'Color value', 'woocommerce' );
+			}
+			$new_columns[ $key ] = $label;
+		}
+
+		if ( ! isset( $new_columns['color'] ) ) {
+			$new_columns['color'] = __( 'Color value', 'woocommerce' );
+		}
+
+		return $new_columns;
+	}
+
+	/**
+	 * Render color column for product attribute terms.
+	 *
+	 * @param string $columns Existing columns HTML.
+	 * @param string $column  Current column key.
+	 * @param int    $term_id Term ID.
+	 * @return string
+	 *
+	 * @internal
+	 */
+	public function render_term_color_column( $columns, $column, $term_id ) {
+		if ( 'color' !== $column ) {
+			return $columns;
+		}
+
+		$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( ! $this->is_visual_product_attribute_taxonomy( $taxonomy ) ) {
+			return $columns;
+		}
+
+		$color_value = get_term_meta( $term_id, 'color', true );
+
+		if ( ! $color_value ) {
+			return '&ndash;';
+		}
+
+		$color_value = sanitize_hex_color( $color_value );
+
+		if ( ! $color_value ) {
+			return '&ndash;';
+		}
+
+		$swatch = sprintf(
+			'<span class="wc-admin-color-swatch" style="background-color:%s;" aria-hidden="true"></span>',
+			esc_attr( $color_value )
+		);
+
+		return $swatch . esc_html( strtoupper( $color_value ) );
+	}
+
 	/**
 	 * Thumbnail column added to category admin.
 	 *
@@ -477,33 +627,70 @@ class WC_Admin_Taxonomies {
 	 * @return void
 	 */
 	public function scripts_at_product_cat_screen_footer() {
-		if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok.
+		$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( 'product_cat' !== $taxonomy ) {
 			return;
 		}

-		// Ensure the tooltip is displayed when the image column is disabled on product categories.
 		$handle = 'wc-admin-taxonomies';
 		wp_register_script( $handle, '', array(), WC_VERSION, array( 'in_footer' => true ) );
 		wp_enqueue_script( $handle );
+
+		// Ensure the tooltip is displayed when the image column is disabled on product categories.
 		wp_add_inline_script(
 			$handle,
 			sprintf(
 				"(function() {
-                    'use strict';
-                    const product_cat = document.getElementById('tag-%d');
-                    if (product_cat) {
-                        const th = product_cat.querySelector('th');
-                        const thumbSpan = product_cat.querySelector('td.thumb span');
-                        if (th && thumbSpan) {
-                            th.innerHTML = '';
-                            th.appendChild(thumbSpan);
-                        }
-                    }
-                })();",
+					'use strict';
+					const product_cat = document.getElementById('tag-%d');
+					if (product_cat) {
+						const th = product_cat.querySelector('th');
+						const thumbSpan = product_cat.querySelector('td.thumb span');
+						if (th && thumbSpan) {
+							th.innerHTML = '';
+							th.appendChild(thumbSpan);
+						}
+					}
+				})();",
 				absint( $this->default_cat_id )
 			)
 		);
 	}
+
+	/**
+	 * Admin footer scripts for visual attribute taxonomy screens.
+	 *
+	 * @return void
+	 *
+	 * @internal
+	 */
+	public function scripts_at_visual_attribute_screen_footer() {
+		$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( ! $this->is_visual_product_attribute_taxonomy( $taxonomy ) ) {
+			return;
+		}
+
+		$handle = 'wc-admin-visual-attribute';
+		wp_register_script( $handle, '', array(), WC_VERSION, array( 'in_footer' => true ) );
+		wp_enqueue_script( $handle );
+		wp_add_inline_script(
+			$handle,
+			"(function() {
+				'use strict';
+				const addFormColor = document.querySelector('.form-field.term-color-wrap');
+				const addFormSlug = document.querySelector('.form-field.term-slug-wrap');
+				if (addFormColor && addFormSlug) {
+					addFormSlug.parentNode.insertBefore(addFormColor, addFormSlug);
+				}
+
+				const editFormColor = document.querySelector('tr.form-field.term-color-wrap');
+				const editFormSlug = document.querySelector('tr.form-field.term-slug-wrap');
+				if (editFormColor && editFormSlug) {
+					editFormSlug.parentNode.insertBefore(editFormColor, editFormSlug);
+				}
+			})();"
+		);
+	}
 }

 $wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance();
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
index 9d742f1e11f..0b9fca21e01 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
@@ -35,7 +35,10 @@ if ( ! defined( 'ABSPATH' ) ) {
 					$attribute_taxonomy->attribute_type = 'select';
 				}

-				if ( 'select' === $attribute_taxonomy->attribute_type ) {
+				if (
+					'select' === $attribute_taxonomy->attribute_type ||
+					'wc-visual' === $attribute_taxonomy->attribute_type
+				) {
 					$attribute_orderby = ! empty( $attribute_taxonomy->attribute_orderby ) ? $attribute_taxonomy->attribute_orderby : 'name';
 					/**
 					* Filter the length (number of terms) rendered in the list.
diff --git a/plugins/woocommerce/includes/wc-attribute-functions.php b/plugins/woocommerce/includes/wc-attribute-functions.php
index cb043264cce..bcbbae57f05 100644
--- a/plugins/woocommerce/includes/wc-attribute-functions.php
+++ b/plugins/woocommerce/includes/wc-attribute-functions.php
@@ -211,7 +211,7 @@ function wc_attribute_label( $name, $product = '' ) {
 		}
 	} else {
 		$label = $name;
-	}
+	}//end if

 	return apply_filters( 'woocommerce_attribute_label', $label, $name, $product );
 }
@@ -253,16 +253,38 @@ function wc_get_attribute_taxonomy_names() {
  * @return array
  */
 function wc_get_attribute_types() {
+	$attribute_types = array(
+		'select' => __( 'Text', 'woocommerce' ),
+	);
+
+	$allow_visual_attribute_type =
+		wp_is_block_theme() &&
+		\Automattic\WooCommerce\Admin\Features\Features::is_enabled( 'wc-visual-attribute' );
+
+	// If the store already has some visual attributes, let's allow them even
+	// if the current theme is not a block theme.
+	if ( ! $allow_visual_attribute_type ) {
+		foreach ( wc_get_attribute_taxonomies() as $attribute_taxonomy ) {
+			if ( isset( $attribute_taxonomy->attribute_type ) && 'wc-visual' === $attribute_taxonomy->attribute_type ) {
+				$allow_visual_attribute_type = true;
+				break;
+			}
+		}
+	}
+
+	if ( $allow_visual_attribute_type ) {
+		$attribute_types['wc-visual'] = __( 'Color / Image', 'woocommerce' );
+	}
+
 	return (array) apply_filters(
 		'product_attributes_type_selector',
-		array(
-			'select' => __( 'Select', 'woocommerce' ),
-		)
+		$attribute_types
 	);
 }

 /**
- * Check if there are custom attribute types.
+ * Check if there are attribute types different than the default `select` type.
+ * Note: `wc-visual` is considered a custom attribute type.
  *
  * @since  3.3.2
  * @return bool True if there are custom types, otherwise false.
@@ -283,7 +305,7 @@ function wc_has_custom_attribute_types() {
 function wc_get_attribute_type_label( $type ) {
 	$types = wc_get_attribute_types();

-	return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Select', 'woocommerce' );
+	return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Text', 'woocommerce' );
 }

 /**
@@ -621,8 +643,8 @@ function wc_create_attribute( $args ) {
 			if ( isset( $wp_taxonomies[ $old_taxonomy_name ] ) && ! isset( $wp_taxonomies[ $new_taxonomy_name ] ) ) {
 				$wp_taxonomies[ $new_taxonomy_name ] = $wp_taxonomies[ $old_taxonomy_name ]; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
 			}
-		}
-	}
+		}//end if
+	}//end if

 	// Clear cache and flush rewrite rules.
 	wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
@@ -730,7 +752,7 @@ function wc_delete_attribute( $id ) {
 		WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

 		return true;
-	}
+	}//end if

 	return false;
 }
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
index b15b68c2b93..21d95528fa9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
@@ -3,7 +3,6 @@ declare(strict_types=1);

 namespace Automattic\WooCommerce\Blocks\BlockTypes;

-use Automattic\WooCommerce\Admin\Features\Features;
 use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
 use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils;
 use Automattic\WooCommerce\Enums\ProductType;
diff --git a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
index 2b4a1193648..1b509fd30bd 100644
--- a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
@@ -29,7 +29,7 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
 		$this->filter_recorder = $this->any();

 		$filter_mock = $this->getMockBuilder( stdClass::class )
-			->setMethods( [ '__invoke' ] )
+			->setMethods( array( '__invoke' ) )
 			->getMock();
 		$filter_mock->expects( $this->filter_recorder )
 			->method( '__invoke' )
@@ -55,14 +55,14 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
 	 */
 	public function test_wc_get_attribute_taxonomy_ids() {
 		$ids = wc_get_attribute_taxonomy_ids();
-		$this->assertEquals( [], $ids );
+		$this->assertEquals( array(), $ids );
 		$this->assertEquals(
 			1,
 			$this->filter_recorder->getInvocationCount(),
 			'Filter `woocommerce_attribute_taxonomies` should have been triggered once after fetching all attribute taxonomies.'
 		);
 		$ids = wc_get_attribute_taxonomy_ids();
-		$this->assertEquals( [], $ids );
+		$this->assertEquals( array(), $ids );
 		$this->assertEquals(
 			1,
 			$this->filter_recorder->getInvocationCount(),
@@ -76,14 +76,14 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
 	 */
 	public function test_wc_get_attribute_taxonomy_labels() {
 		$labels = wc_get_attribute_taxonomy_labels();
-		$this->assertEquals( [], $labels );
+		$this->assertEquals( array(), $labels );
 		$this->assertEquals(
 			1,
 			$this->filter_recorder->getInvocationCount(),
 			'Filter `woocommerce_attribute_taxonomies` should have been triggered once after fetching all attribute taxonomies.'
 		);
 		$labels = wc_get_attribute_taxonomy_labels();
-		$this->assertEquals( [], $labels );
+		$this->assertEquals( array(), $labels );
 		$this->assertEquals(
 			1,
 			$this->filter_recorder->getInvocationCount(),
@@ -215,12 +215,60 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
 		$this->assertEquals( 'menu_order', $attribute->order_by, 'Any invalid property changes will be reset to their defaults.' );
 	}

+	/**
+	 * Test visual attribute type registration and persistence.
+	 *
+	 * @testdox Should have the `wc-visual` attribute type registered in block themes.
+	 */
+	public function test_wc_visual_attribute_type() {
+		$original_theme = wp_get_theme()->get_stylesheet();
+		$attribute_id   = null;
+
+		$enable_visual_attribute_feature = function ( $features ) {
+			$features[] = 'wc-visual-attribute';
+			return array_unique( $features );
+		};
+
+		add_filter( 'woocommerce_admin_features', $enable_visual_attribute_feature );
+		try {
+			switch_theme( 'twentytwentyfour' );
+
+			$this->assertArrayHasKey( 'wc-visual', wc_get_attribute_types(), 'The visual attribute type should be available in block themes.' );
+
+			$attribute_id = wc_create_attribute(
+				array(
+					'name' => 'Visual Color',
+					'type' => 'wc-visual',
+				)
+			);
+
+			$this->assertIsInt( $attribute_id );
+			$this->assertEquals( 'wc-visual', wc_get_attribute( $attribute_id )->type, 'The attribute type should be `wc-visual` in block themes.' );
+
+			switch_theme( 'storefront' );
+			$this->assertEquals( 'wc-visual', wc_get_attribute( $attribute_id )->type, 'The attribute type should be `wc-visual` in classic themes.' );
+			$this->assertArrayHasKey( 'wc-visual', wc_get_attribute_types(), 'The visual attribute type should be available in classic themes with a visual attribute.' );
+
+			wc_delete_attribute( $attribute_id );
+			$attribute_id = null;
+
+			$this->assertArrayNotHasKey( 'wc-visual', wc_get_attribute_types(), 'The visual attribute type should not be available in classic themes without a visual attribute.' );
+		} finally {
+			if ( is_int( $attribute_id ) ) {
+				wc_delete_attribute( $attribute_id );
+			}
+
+			remove_filter( 'woocommerce_admin_features', $enable_visual_attribute_feature );
+			switch_theme( $original_theme );
+		}//end try
+	}
+
 	public function get_attribute_names_and_slugs() {
-		return [
-			[ 'Dash Me', 'dash-me' ],
-			[ '', '' ],
-			[ 'pa_SubStr', 'substr' ],
-			[ 'ĂnîC°Dę', 'anicde' ],
-		];
+		return array(
+			array( 'Dash Me', 'dash-me' ),
+			array( '', '' ),
+			array( 'pa_SubStr', 'substr' ),
+			array( 'ĂnîC°Dę', 'anicde' ),
+		);
 	}
 }