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 );
+ }
+ }
+}