Commit 57793e6acbc for woocommerce

commit 57793e6acbc6e5056bea3534376cafc197a68c59
Author: Faisal Ahammad <faisalahammad24@gmail.com>
Date:   Wed Jul 8 14:35:00 2026 +0600

    Add default color terms when creating a wc-visual attribute (#65923)

    * feat: auto-seed default color terms for new wc-visual attributes

    Adds a static seeder that creates 9 default color terms when a
    wc-visual attribute is created from the admin. The hook reads the
    attribute data passed by woocommerce_attribute_added instead of
    re-querying wc_get_attribute(), which is not yet cached at that point.

    Fixes #65912

    * fix(product-attributes): strengthen negative ID guard in seed_visual_attribute_terms

    - Change guard from 0 ===  to 0 >=
    - Rejects negative IDs in the public static method
    - Aligns with VisualAttributeTermAdmin::maybe_seed_visual_attribute_terms guard

    Addresses PR feedback.

    Refs #65923

    * fix: make default color term names translatable

    Replace hardcoded DEFAULT_COLOR_TERMS constant with
    get_default_color_terms() static method using __().
    Canonical English slugs keep re-seeding idempotent
    across locales; display names are translatable.

    Follows WC_Install::create_terms() and
    FulfillmentUtils::get_default_order_fulfillment_statuses()
    i18n pattern.

    Addresses PR review feedback.

    Refs #65923

    * fix(product-attributes): address PR review feedback

    - Remove redundant guard in VisualAttributeTermAdmin wrapper.
    - Strengthen seed_visual_attribute_terms guard with is_string checks.
    - Add inline comment explaining register_taxonomy fallback.
    - Remove verbose PHPDoc paragraph.

    Refs #65923.

    * fix(product-attributes): move default color term seeding to admin-only path

    - Remove woocommerce_attribute_added hook and wrapper from VisualAttributeTermAdmin.
    - Move seed_visual_attribute_terms() and default colors from VisualAttributeTermMeta to VisualAttributeTermAdmin.
    - Call seeder from WC_Admin_Attributes::process_add_attribute() after wc_create_attribute succeeds.
    - Validate taxonomy against the created attribute ID before inserting terms.
    - Move seeding tests to VisualAttributeTermAdminTest and use deterministic unique suffixes.

    Addresses PR review feedback.

    Refs #65923

    * fix(product-attributes): trust persisted attribute type and harden seeder tests

    - Verify wc_get_attribute() returns wc-visual type before seeding; do not trust
      caller-supplied attribute_type which could be spoofed by a custom path.
    - Tighten test_seeds_default_color_terms_for_wc_visual_attribute to assert exact
      slug -> hex map for all 9 defaults instead of a permissive hex regex.
    - Add test_seeds_when_taxonomy_is_not_registered to cover the register_taxonomy
      fallback branch executed by WC_Admin_Attributes::process_add_attribute().
    - Add product_attributes_type_selector filter in setUp/tearDown so wc-visual is
      always in the allowed type list; wc_create_attribute() otherwise coerces it to
      select when the test environment lacks a block theme + feature flag.

    Addresses CodeRabbit review feedback.

    Refs #65923

    * fix(product-attributes): address Aljullu review feedback

    - Drop @since 11.0.0 from private method docblocks (T1)
    - Remove  param from seed_visual_attribute_terms (T2)
    - Update test call sites to match new signature (T2)
    - Add feature flag gate via FeaturesUtil in register() and is_visual_attribute_taxonomy() (T3)
    - Wrap register_taxonomy() with WC filter seams (T4)

    Addresses PR #65923.

    * fix(product-attributes): gate wc-visual behavior on stored type, not feature flag

    - Remove feature-flag check from VisualAttributeTermMeta::is_visual_attribute_taxonomy()
    - Remove feature-flag check from VisualAttributeTermAdmin::register()
    - Drop dead $attribute arg from seed_visual_attribute_terms() call
    - Remove now-unused FeaturesUtil imports

    Refs #65923

    * fix: align equals sign in VisualAttributeTermAdmin to satisfy phpcs

    - Fixes Generic.Formatting.MultipleStatementAlignment warning at line 495
    - $args = (single space) instead of padded

    Covers branch-level lint:changes:branch during PR review.

    Refs #65923

    * test: use feature flag + block theme setup for visual attribute tests

    Align with wc-attribute-functions-test.php pattern (Aljullu review thread
    #3526988127): switch to block theme twentytwentyfour and enable
    wc-visual-attribute via FeaturesController instead of filtering
    product_attributes_type_selector directly.

    * test: remove unnecessary pre-existing terms test

    Removing test_seeds_only_missing_default_color_terms per review feedback.
    The seeder only runs immediately after attribute creation, so pre-existing
    terms cannot occur in practice.

    Refs #65923

diff --git a/plugins/woocommerce/changelog/65912-add-visual-attribute-default-color-terms b/plugins/woocommerce/changelog/65912-add-visual-attribute-default-color-terms
new file mode 100644
index 00000000000..ae2d499936f
--- /dev/null
+++ b/plugins/woocommerce/changelog/65912-add-visual-attribute-default-color-terms
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Automatically create 9 default color terms when a new wc-visual product attribute is created, so users can start using color swatches without manually adding each color.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
index 7b4971f18a4..400c4a0e360 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
@@ -10,6 +10,8 @@

 defined( 'ABSPATH' ) || exit;

+use Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermAdmin;
+
 /**
  * WC_Admin_Attributes Class.
  */
@@ -115,6 +117,8 @@ class WC_Admin_Attributes {
 			return $id;
 		}

+		VisualAttributeTermAdmin::seed_visual_attribute_terms( $id );
+
 		return true;
 	}

diff --git a/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php b/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
index 7d84015dc46..60ed1f25859 100644
--- a/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
+++ b/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
@@ -30,6 +30,7 @@ class VisualAttributeTermAdmin implements RegisterHooksInterface {
 		if ( ! is_admin() ) {
 			return;
 		}
+
 		add_action( 'created_term', array( $this, 'save_product_attribute_term_fields' ), 10, 3 );
 		add_action( 'edit_term', array( $this, 'save_product_attribute_term_fields' ), 10, 3 );

@@ -438,4 +439,121 @@ class VisualAttributeTermAdmin implements RegisterHooksInterface {
 	private function get_current_taxonomy(): string {
 		return isset( $_GET['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 	}
+
+	/**
+	 * Get the default color terms to create for a new wc-visual attribute.
+	 *
+	 * @return array<string, array{label: string, color: string}>
+	 */
+	private static function get_default_color_terms(): array {
+		return array(
+			'black'  => array(
+				'label' => __( 'Black', 'woocommerce' ),
+				'color' => '#121212',
+			),
+			'white'  => array(
+				'label' => __( 'White', 'woocommerce' ),
+				'color' => '#FFFFFF',
+			),
+			'gray'   => array(
+				'label' => __( 'Gray', 'woocommerce' ),
+				'color' => '#6E6E6E',
+			),
+			'red'    => array(
+				'label' => __( 'Red', 'woocommerce' ),
+				'color' => '#D32F2F',
+			),
+			'blue'   => array(
+				'label' => __( 'Blue', 'woocommerce' ),
+				'color' => '#1976D2',
+			),
+			'green'  => array(
+				'label' => __( 'Green', 'woocommerce' ),
+				'color' => '#388E3C',
+			),
+			'yellow' => array(
+				'label' => __( 'Yellow', 'woocommerce' ),
+				'color' => '#FBE02D',
+			),
+			'pink'   => array(
+				'label' => __( 'Pink', 'woocommerce' ),
+				'color' => '#EC407A',
+			),
+			'brown'  => array(
+				'label' => __( 'Brown', 'woocommerce' ),
+				'color' => '#5D4037',
+			),
+		);
+	}
+
+	/**
+	 * Seed default color terms for a newly created wc-visual attribute.
+	 *
+	 * @internal
+	 *
+	 * @param int $attribute_id Attribute ID.
+	 * @return void
+	 */
+	public static function seed_visual_attribute_terms( int $attribute_id ): void {
+		if ( 0 >= $attribute_id ) {
+			return;
+		}
+
+		$attribute = wc_get_attribute( $attribute_id );
+
+		if (
+			! $attribute ||
+			! isset( $attribute->slug, $attribute->type ) ||
+			'wc-visual' !== $attribute->type
+		) {
+			return;
+		}
+
+		$taxonomy = $attribute->slug;
+
+		// Taxonomy is registered on init from the cached list but not yet available
+		// at process_add_attribute time (cache invalidated after wc_create_attribute).
+		// Use the same WC filter seams as WC_Post_Types::register_taxonomies().
+		if ( ! taxonomy_exists( $taxonomy ) ) {
+			/**
+			 * Filters the object types for a WooCommerce taxonomy.
+			 *
+			 * @param array $objects Object types.
+			 *
+			 * @since 11.0.0
+			 */
+			$objects = apply_filters( "woocommerce_taxonomy_objects_{$taxonomy}", array( 'product' ) );
+			/**
+			 * Filters the arguments for a WooCommerce taxonomy.
+			 *
+			 * @param array $args Taxonomy arguments.
+			 *
+			 * @since 11.0.0
+			 */
+			$args = apply_filters(
+				"woocommerce_taxonomy_args_{$taxonomy}",
+				array(
+					'hierarchical' => false,
+					'public'       => false,
+					'show_ui'      => false,
+					'show_in_menu' => false,
+				)
+			);
+			register_taxonomy( $taxonomy, $objects, $args );
+		}
+
+		foreach ( self::get_default_color_terms() as $slug => $term ) {
+			if ( get_term_by( 'slug', $slug, $taxonomy ) ) {
+				continue;
+			}
+
+			$result = wp_insert_term( $term['label'], $taxonomy, array( 'slug' => $slug ) );
+
+			if ( is_wp_error( $result ) || empty( $result['term_id'] ) ) {
+				continue;
+			}
+
+			VisualAttributeTermMeta::save_term_visual( (int) $result['term_id'], $term['color'], 0 );
+		}
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductAttributes/VisualAttributeTermAdminTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductAttributes/VisualAttributeTermAdminTest.php
new file mode 100644
index 00000000000..35dfd5b0d7c
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductAttributes/VisualAttributeTermAdminTest.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * Visual attribute term admin tests.
+ *
+ * @package WooCommerce\Tests\Internal\ProductAttributes
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\ProductAttributes;
+
+use Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermAdmin;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the visual attribute term admin functionality.
+ */
+class VisualAttributeTermAdminTest extends WC_Unit_Test_Case {
+
+	/**
+	 * Original theme stylesheet to restore after tests.
+	 *
+	 * @var string|null
+	 */
+	private $original_theme;
+
+	/**
+	 * Set up block theme + enable wc-visual feature so 'wc-visual' type
+	 * is available via the real wc_get_attribute_types() logic.
+	 * Matches the approach used in other visual attribute tests.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->original_theme = wp_get_theme()->get_stylesheet();
+
+		// wc-visual requires block theme + feature. Enable for the duration
+		// of the test so wc_create_attribute accepts/stores type 'wc-visual'.
+		switch_theme( 'twentytwentyfour' );
+		delete_option( 'woocommerce_feature_wc_visual_attribute_enabled' );
+		wc_get_container()
+			->get( \Automattic\WooCommerce\Internal\Features\FeaturesController::class )
+			->change_feature_enable( 'wc-visual-attribute', true );
+	}
+
+	/**
+	 * Restore original theme and clean feature option.
+	 */
+	public function tearDown(): void {
+		if ( $this->original_theme ) {
+			switch_theme( $this->original_theme );
+		}
+		delete_option( 'woocommerce_feature_wc_visual_attribute_enabled' );
+		parent::tearDown();
+	}
+
+	/**
+	 * Counter for unique attribute slugs within a test run.
+	 *
+	 * @var int
+	 */
+	private static $attribute_counter = 0;
+
+	/**
+	 * Get a unique suffix for test attribute slugs.
+	 *
+	 * @return string
+	 */
+	private static function get_unique_suffix(): string {
+		return (string) ++self::$attribute_counter;
+	}
+
+	/**
+	 * @testdox Should create 9 default color terms for a new wc-visual attribute.
+	 */
+	public function test_seeds_default_color_terms_for_wc_visual_attribute(): void {
+		$suffix         = self::get_unique_suffix();
+		$attribute_data = array(
+			'name' => 'Seed Visual Test ' . $suffix,
+			'slug' => 'seed-visual-test-' . $suffix,
+			'type' => 'wc-visual',
+		);
+		$attribute_id   = wc_create_attribute( $attribute_data );
+
+		$this->assertIsInt( $attribute_id, 'A wc-visual attribute should be created.' );
+
+		$attribute = wc_get_attribute( $attribute_id );
+		$taxonomy  = $attribute->slug;
+		$term_ids  = array();
+
+		try {
+			register_taxonomy( $taxonomy, array( 'product' ) );
+
+			VisualAttributeTermAdmin::seed_visual_attribute_terms(
+				$attribute_id
+			);
+
+			$terms = get_terms(
+				array(
+					'taxonomy'   => $taxonomy,
+					'hide_empty' => false,
+				)
+			);
+
+			$this->assertIsArray( $terms, 'Terms should be returned for the taxonomy.' );
+			$this->assertCount( 9, $terms, 'Nine default color terms should be created.' );
+
+			foreach ( $terms as $term ) {
+				$term_ids[] = (int) $term->term_id;
+			}
+
+			$black_term = get_term_by( 'slug', 'black', $taxonomy );
+			$this->assertInstanceOf( \WP_Term::class, $black_term, 'Black term should be seeded with the canonical English slug.' );
+			$this->assertSame( __( 'Black', 'woocommerce' ), $black_term->name, 'Term name should be the translated label, not hardcoded English.' );
+
+			$expected_colors = array(
+				'black'  => '#121212',
+				'white'  => '#FFFFFF',
+				'gray'   => '#6E6E6E',
+				'red'    => '#D32F2F',
+				'blue'   => '#1976D2',
+				'green'  => '#388E3C',
+				'yellow' => '#FBE02D',
+				'pink'   => '#EC407A',
+				'brown'  => '#5D4037',
+			);
+
+			foreach ( $expected_colors as $slug => $expected_hex ) {
+				$term = get_term_by( 'slug', $slug, $taxonomy );
+				$this->assertInstanceOf( \WP_Term::class, $term, sprintf( 'Term with slug "%s" should exist.', $slug ) );
+				$this->assertSame(
+					$expected_hex,
+					get_term_meta( $term->term_id, 'color', true ),
+					sprintf( 'Term "%s" should have color "%s".', $slug, $expected_hex )
+				);
+			}
+		} finally {
+			foreach ( $term_ids as $term_id ) {
+				wp_delete_term( $term_id, $taxonomy );
+			}
+			unregister_taxonomy( $taxonomy );
+			wc_delete_attribute( $attribute_id );
+		}
+	}
+
+	/**
+	 * @testdox Should auto-register the taxonomy and seed color terms when called before the taxonomy is registered.
+	 */
+	public function test_seeds_when_taxonomy_is_not_registered(): void {
+		$suffix         = self::get_unique_suffix();
+		$attribute_data = array(
+			'name' => 'Seed Visual Unregistered ' . $suffix,
+			'slug' => 'seed-visual-unregistered-' . $suffix,
+			'type' => 'wc-visual',
+		);
+		$attribute_id   = wc_create_attribute( $attribute_data );
+
+		$this->assertIsInt( $attribute_id, 'A wc-visual attribute should be created.' );
+
+		$taxonomy = wc_attribute_taxonomy_name( $attribute_data['slug'] );
+		$term_ids = array();
+
+		// Pre-assert the fallback branch is actually exercised; if WC auto-registers
+		// the taxonomy on init the test would silently bypass the code under test.
+		$this->assertFalse(
+			taxonomy_exists( $taxonomy ),
+			'Taxonomy should not be registered before the seeder runs; otherwise the fallback branch is not exercised.'
+		);
+
+		try {
+			VisualAttributeTermAdmin::seed_visual_attribute_terms(
+				$attribute_id
+			);
+
+			$this->assertTrue( taxonomy_exists( $taxonomy ), 'Seeder should register the taxonomy when missing.' );
+
+			$terms = get_terms(
+				array(
+					'taxonomy'   => $taxonomy,
+					'hide_empty' => false,
+				)
+			);
+
+			$this->assertIsArray( $terms, 'Terms should be returned for the taxonomy.' );
+			$this->assertCount( 9, $terms, 'Nine default color terms should be seeded after auto-registering the taxonomy.' );
+
+			foreach ( $terms as $term ) {
+				$term_ids[] = (int) $term->term_id;
+			}
+		} finally {
+			foreach ( $term_ids as $term_id ) {
+				wp_delete_term( $term_id, $taxonomy );
+			}
+			unregister_taxonomy( $taxonomy );
+			wc_delete_attribute( $attribute_id );
+		}
+	}
+
+	/**
+	 * @testdox Should not create color terms for non-wc-visual attribute types.
+	 */
+	public function test_does_not_seed_for_non_wc_visual_attribute(): void {
+		$suffix         = self::get_unique_suffix();
+		$attribute_data = array(
+			'name' => 'Seed Select Test ' . $suffix,
+			'slug' => 'seed-select-test-' . $suffix,
+			'type' => 'select',
+		);
+		$attribute_id   = wc_create_attribute( $attribute_data );
+
+		$this->assertIsInt( $attribute_id, 'A select attribute should be created.' );
+
+		$attribute = wc_get_attribute( $attribute_id );
+		$taxonomy  = $attribute->slug;
+
+		try {
+			register_taxonomy( $taxonomy, array( 'product' ) );
+
+			VisualAttributeTermAdmin::seed_visual_attribute_terms(
+				$attribute_id
+			);
+
+			$terms = get_terms(
+				array(
+					'taxonomy'   => $taxonomy,
+					'hide_empty' => false,
+				)
+			);
+
+			$this->assertIsArray( $terms, 'get_terms should return an array.' );
+			$this->assertCount( 0, $terms, 'No default color terms should be created for a select attribute.' );
+		} finally {
+			unregister_taxonomy( $taxonomy );
+			wc_delete_attribute( $attribute_id );
+		}
+	}
+}