Commit b54f9602dce for woocommerce
commit b54f9602dce2a43fd1616f3f14a1226f96ad9fd4
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Thu May 7 13:09:08 2026 +0300
Add accessible star-rating control for Review Order page (#64525)
* Add accessible star-rating control for Review Order page
Native `<input type="radio">` ✕ 5 wrapped in a `role="radiogroup"` container,
visually replaced by SVG stars. Progressive enhancement adds:
- Keyboard nav: ArrowLeft/Up, ArrowRight/Down, Home, End. Selecting also
moves focus and dispatches `change`, so any future submission listener
reacts naturally.
- Dynamic caption underneath the stars, populated from the focused/checked
radio's `data-label`. Defaults match the existing WC product-review
labels (Very poor / Not that bad / Average / Good / Perfect) and are
filterable via `woocommerce_review_order_rating_labels`.
- High-contrast focus ring on the wrapping label whenever its input is
`:focus-visible`.
- Hover preview lights stars left of the cursor; selection persists when
the cursor leaves.
Without JavaScript the underlying radio group still works as a plain
form field, so the page stays functional.
Files:
- `src/Internal/OrderReviews/StarRating.php` — server-side renderer.
Builds the markup and exposes `get_labels()` (filter-aware, with safe
fallbacks for buggy filters that drop slot keys).
- `templates/order/star-rating.php` — theme-overridable partial.
- `client/legacy/js/frontend/order-review.js` — vanilla JS, no jQuery.
- `client/legacy/css/order-review.scss` — visual states.
- `Internal/OrderReviews/Endpoint::enqueue_assets()` — script + style
enqueued only when the review-order endpoint is actually rendering.
- `templates/order/customer-review-order.php` — temporary preview render
per item so reviewers can interact with the control. M4 (WOOPLUG-6595)
swaps this preview for the real per-item form row.
Tests:
- `Internal\OrderReviews\StarRatingTest` — markup, defaults, filter
override, filter fallback for missing keys.
Closes #64308 (WOOPLUG-6594).
* Add changefile(s) from automation for the following project(s): woocommerce
* Remove duplicate changelog file (workflow generates the canonical one)
* Address Copilot feedback on star-rating control PR
- Route enqueue URLs through woocommerce_get_asset_url filter so
CDN/asset-rewrite setups continue to work.
- Add screen-reader-only 'Required' text alongside the visual asterisk
on the rating label.
- Correct the JS file header comment: only Arrow keys + Home/End are
handled, not Space/Enter.
* Fix StarRating docblock to use flat @param syntax
The PHPDoc shape syntax (array{...}) trips the WC PHPCS sniff, which
expects a single @param line per parameter. Reverted to plain
@param array with the key descriptions in the body.
* Drop the planning comment + minor tidy on star-rating styles
* Load the RTL stylesheet variant on RTL sites
wp_style_add_data( 'wc-order-review', 'rtl', 'replace' ) tells the
classic-assets pipeline to swap order-review.css for order-review-rtl.css
on right-to-left locales.
* Trim defensive fallbacks and dedupe asset enqueue helpers
- StarRating::get_labels(): replace the manual 1-5 fallback loop and
reorder block with array_replace + array_intersect_key.
- StarRating::render(): drop the unused $selected pre-cast; read it
inline only when it's used.
- Endpoint::enqueue_assets(): factor a local closure for the
woocommerce_get_asset_url filter so we don't repeat the same call
for the style and the script.
- order/star-rating.php template: drop the data-caption-id attribute
that duplicated aria-describedby.
- order-review.js: read the caption id from aria-describedby and drop
the empty-input early-out (init() already filters by class).
No behaviour change. All six StarRating tests still pass.
* Address review feedback on the star-rating control
- Add @since 10.8.0 annotations on StarRating::render() and
StarRating::get_labels() docblocks (per ayushpahwa).
- Bound the `selected` argument: values outside 0-5 fall back to
no-selection rather than passing an unrenderable value to the
template (per ayushpahwa).
- Cover the bound with a regression test.
* Address remaining review feedback on the star-rating control
- CSS: drop every `:has()` selector. The template now renders the
inputs in reverse DOM order (5..1) and uses `flex-direction:
row-reverse`; selected/hover/focus states are driven by the `~`
sibling combinator and `:focus-visible + label`. Caption gets
`order: -1` so it stays on the visual right.
- JS: keyboard navigation flips so ArrowRight/Down still moves
visually right (DOM-previous) and Home/End map to the visual
leftmost/rightmost stars.
- Template: pre-compute `$initial_caption` so the caption element no
longer mixes selection logic, escaping, and output in a single
expression.
- Endpoint: enqueue the JS with `strategy => defer` (in addition to
`in_footer => true`) since it only attaches DOM-ready listeners.
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/64525-wooplug-6594-accessible-star-rating-control b/plugins/woocommerce/changelog/64525-wooplug-6594-accessible-star-rating-control
new file mode 100644
index 00000000000..5813d0415f7
--- /dev/null
+++ b/plugins/woocommerce/changelog/64525-wooplug-6594-accessible-star-rating-control
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add the accessible 5-star rating control used by the Review Order page. Native radio inputs with keyboard navigation (arrows, Home, End), dynamic caption, and visible focus ring; filterable labels via `woocommerce_review_order_rating_labels`.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/legacy/css/order-review.scss b/plugins/woocommerce/client/legacy/css/order-review.scss
new file mode 100644
index 00000000000..06ea797fe44
--- /dev/null
+++ b/plugins/woocommerce/client/legacy/css/order-review.scss
@@ -0,0 +1,85 @@
+/**
+ * Review Order page styles.
+ *
+ * Intentionally minimal. The active theme drives typography, color, and
+ * button styling; only the interactive star-rating control needs bespoke
+ * CSS to map radio inputs to SVG stars.
+ *
+ * The star control renders with `flex-direction: row-reverse`: the DOM
+ * order is 5..1 so that sibling combinators (`~`) can fill all stars up
+ * to the selected/hovered one without relying on `:has()`.
+ */
+
+.woocommerce-star-rating {
+ display: inline-flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 2px;
+ line-height: 1;
+
+ // Visually hide the native radios but keep them in the a11y tree.
+ &__input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ &__star {
+ cursor: pointer;
+ display: inline-flex;
+ padding: 4px;
+ color: currentColor;
+ }
+
+ &__icon {
+ display: block;
+ width: 1.5em;
+ height: 1.5em;
+ fill: currentColor;
+ opacity: 0.3;
+ transition: opacity 100ms ease-in-out;
+ }
+
+ // Fill: a checked input fills its own label and every following sibling
+ // star (which appear visually before it under row-reverse).
+ &__input:checked ~ .woocommerce-star-rating__star .woocommerce-star-rating__icon {
+ opacity: 1;
+ }
+
+ // Hover preview: when hovering the rating, dim everything, then re-fill
+ // from the hovered star outward (visual left).
+ &:hover .woocommerce-star-rating__icon {
+ opacity: 0.3;
+ }
+
+ &__star:hover .woocommerce-star-rating__icon,
+ &__star:hover ~ .woocommerce-star-rating__star .woocommerce-star-rating__icon {
+ opacity: 1;
+ }
+
+ // Focus ring: the input sits next to its own label; style the label when
+ // the input has visible keyboard focus.
+ &__input:focus-visible + .woocommerce-star-rating__star {
+ outline: 2px solid currentColor;
+ outline-offset: 2px;
+ }
+
+ // Caption keeps a stable footprint so the layout doesn't shift when the
+ // label text changes. Negative order sends it to the visual right of the
+ // stars (under row-reverse, the lowest-ordered item lays out first along
+ // the main axis, which is the rightmost slot).
+ &__caption {
+ order: -1;
+ margin-left: 0.5em;
+ min-height: 1em;
+ min-width: 4em;
+ }
+}
diff --git a/plugins/woocommerce/client/legacy/js/frontend/order-review.js b/plugins/woocommerce/client/legacy/js/frontend/order-review.js
new file mode 100644
index 00000000000..9106800c736
--- /dev/null
+++ b/plugins/woocommerce/client/legacy/js/frontend/order-review.js
@@ -0,0 +1,86 @@
+/* global document, window */
+/**
+ * Progressive enhancement for the Review Order page.
+ *
+ * Adds keyboard navigation (Left/Right/Up/Down + Home/End) and a dynamic
+ * caption to every `.woocommerce-star-rating` group on the page.
+ * Without this script the underlying native radio inputs still work.
+ */
+( function () {
+ 'use strict';
+
+ /**
+ * @param {HTMLElement} container `.woocommerce-star-rating` element.
+ */
+ function initGroup( container ) {
+ var inputs = Array.prototype.slice.call(
+ container.querySelectorAll( '.woocommerce-star-rating__input' )
+ );
+ var captionId = container.getAttribute( 'aria-describedby' );
+ var caption = captionId ? document.getElementById( captionId ) : null;
+
+ function syncCaption() {
+ if ( ! caption ) {
+ return;
+ }
+ var checked = inputs.filter( function ( input ) {
+ return input.checked;
+ } )[ 0 ];
+ caption.textContent = checked
+ ? checked.getAttribute( 'data-label' ) || ''
+ : '';
+ }
+
+ function focusInput( input ) {
+ input.focus();
+ input.checked = true;
+ input.dispatchEvent( new window.Event( 'change', { bubbles: true } ) );
+ }
+
+ // DOM order is 5..1 (reversed for the CSS row-reverse layout), so
+ // "next visual star" is the previous DOM input and vice-versa.
+ // Home/End map to visual-leftmost / visual-rightmost = inputs[last] /
+ // inputs[0] in DOM order.
+ inputs.forEach( function ( input, index ) {
+ input.addEventListener( 'change', syncCaption );
+
+ input.addEventListener( 'keydown', function ( event ) {
+ var nextIndex = null;
+ switch ( event.key ) {
+ case 'ArrowRight':
+ case 'ArrowDown':
+ nextIndex =
+ ( index - 1 + inputs.length ) % inputs.length;
+ break;
+ case 'ArrowLeft':
+ case 'ArrowUp':
+ nextIndex = ( index + 1 ) % inputs.length;
+ break;
+ case 'Home':
+ nextIndex = inputs.length - 1;
+ break;
+ case 'End':
+ nextIndex = 0;
+ break;
+ default:
+ return;
+ }
+ event.preventDefault();
+ focusInput( inputs[ nextIndex ] );
+ } );
+ } );
+
+ syncCaption();
+ }
+
+ function init() {
+ var groups = document.querySelectorAll( '.woocommerce-star-rating' );
+ Array.prototype.forEach.call( groups, initGroup );
+ }
+
+ if ( document.readyState === 'loading' ) {
+ document.addEventListener( 'DOMContentLoaded', init );
+ } else {
+ init();
+ }
+} )();
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
index b628a3f9759..4e336a01871 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
@@ -7,6 +7,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\OrderReviews;
+use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Enums\OrderStatus;
use WC_Order;
use WP_Post;
@@ -251,6 +252,10 @@ class Endpoint {
$this->render_404();
exit;
}
+
+ // template_redirect fires after wp_enqueue_scripts but before
+ // wp_head, so styles registered here are still output in <head>.
+ $this->enqueue_assets();
}
/**
@@ -399,6 +404,37 @@ class Endpoint {
return true;
}
+ /**
+ * Enqueue the JS and CSS that progressively enhance the page.
+ *
+ * Both files live under `client/legacy/` and are built into
+ * `assets/{js|css}/` by the classic-assets pipeline.
+ */
+ private function enqueue_assets(): void {
+ $plugin_url = untrailingslashit( plugins_url( '', WC_PLUGIN_FILE ) );
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+ $version = Constants::get_constant( 'WC_VERSION' );
+ $asset_url = static function ( string $path ) use ( $plugin_url ): string {
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- documented in includes/class-wc-frontend-scripts.php.
+ return (string) apply_filters( 'woocommerce_get_asset_url', $plugin_url . $path, $path );
+ };
+
+ wp_enqueue_style( 'wc-order-review', $asset_url( '/assets/css/order-review.css' ), array(), $version );
+ // Tell WP to swap to the *-rtl.css variant on RTL sites.
+ wp_style_add_data( 'wc-order-review', 'rtl', 'replace' );
+
+ wp_enqueue_script(
+ 'wc-order-review',
+ $asset_url( '/assets/js/frontend/order-review' . $suffix . '.js' ),
+ array(),
+ $version,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ }
+
/**
* Mark the current request as a 404 and load the theme's 404 template.
*
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/StarRating.php b/plugins/woocommerce/src/Internal/OrderReviews/StarRating.php
new file mode 100644
index 00000000000..b30b929f5d1
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/OrderReviews/StarRating.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * StarRating control class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\OrderReviews;
+
+/**
+ * Server-side renderer for the accessible 5-star rating control used on the
+ * Review Order page.
+ *
+ * The control degrades to native `<input type="radio">` elements without
+ * JavaScript and is enhanced by `client/legacy/js/frontend/order-review.js`
+ * for keyboard navigation, a visible focus ring, and the dynamic caption
+ * underneath the stars.
+ *
+ * @internal Just for internal use.
+ *
+ * @since 10.8.0
+ */
+class StarRating {
+
+ /**
+ * Render the rating control. Returns the HTML; does not echo.
+ *
+ * Required keys in `$args`:
+ * - `name` (string): form field name (one per item).
+ * - `id_prefix` (string): prefix used to build unique radio ids.
+ * - `label_id` (string): id of the existing label element that describes
+ * the group via `aria-labelledby`.
+ *
+ * Optional:
+ * - `selected` (int): pre-selected value 0-5; pass `0` (the default)
+ * for no pre-selection. Values outside 0-5 are treated as no selection.
+ *
+ * @since 10.8.0
+ *
+ * @param array $args Render arguments. See description for required and optional keys.
+ * @return string
+ */
+ public static function render( array $args ): string {
+ $name = (string) ( $args['name'] ?? '' );
+ $id_prefix = (string) ( $args['id_prefix'] ?? '' );
+ $label_id = (string) ( $args['label_id'] ?? '' );
+
+ if ( '' === $name || '' === $id_prefix || '' === $label_id ) {
+ return '';
+ }
+
+ $selected = (int) ( $args['selected'] ?? 0 );
+ if ( $selected < 0 || $selected > 5 ) {
+ $selected = 0;
+ }
+
+ ob_start();
+ wc_get_template(
+ 'order/star-rating.php',
+ array(
+ 'name' => $name,
+ 'id_prefix' => $id_prefix,
+ 'label_id' => $label_id,
+ 'selected' => $selected,
+ 'labels' => self::get_labels(),
+ )
+ );
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * Default rating labels, indexed 1-5, after the customer-facing filter.
+ *
+ * Defaults match the long-standing WooCommerce product-review labels in
+ * `templates/single-product-reviews.php` so customers see consistent
+ * wording across the two review entry points.
+ *
+ * @since 10.8.0
+ *
+ * @return array<int, string>
+ */
+ public static function get_labels(): array {
+ $labels = array(
+ 1 => __( 'Very poor', 'woocommerce' ),
+ 2 => __( 'Not that bad', 'woocommerce' ),
+ 3 => __( 'Average', 'woocommerce' ),
+ 4 => __( 'Good', 'woocommerce' ),
+ 5 => __( 'Perfect', 'woocommerce' ),
+ );
+
+ /**
+ * Filter the labels shown under the star-rating control.
+ *
+ * @since 10.8.0
+ *
+ * @param array<int, string> $labels Map of rating value (1-5) to label.
+ */
+ $filtered = (array) apply_filters( 'woocommerce_review_order_rating_labels', $labels );
+
+ // Keep only known 1-5 keys; fall back to defaults for any the filter dropped.
+ return array_replace( $labels, array_intersect_key( $filtered, $labels ) );
+ }
+}
diff --git a/plugins/woocommerce/templates/order/star-rating.php b/plugins/woocommerce/templates/order/star-rating.php
new file mode 100644
index 00000000000..6941b759054
--- /dev/null
+++ b/plugins/woocommerce/templates/order/star-rating.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Star-rating control partial.
+ *
+ * Theme-overridable. Copy to `yourtheme/woocommerce/order/star-rating.php`.
+ *
+ * Renders five native `<input type="radio">` elements wrapped in a
+ * `role="radiogroup"` container, with SVG icons added as decorative siblings
+ * for the visual stars and a caption span the JS module updates as the
+ * selection changes. Without JavaScript the customer still has a working
+ * (if visually plain) radio group.
+ *
+ * @see https://woocommerce.com/document/template-structure/
+ * @package WooCommerce\Templates
+ * @version 10.8.0
+ *
+ * @var string $name Form field name (e.g. `reviews[123][rating]`).
+ * @var string $id_prefix Prefix used for unique radio ids.
+ * @var string $label_id Existing label id; bound via aria-labelledby.
+ * @var int $selected Pre-selected value (0 = none).
+ * @var array<int, string> $labels Map of value (1-5) to caption text.
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+$caption_id = $id_prefix . '-caption';
+$initial_caption = $selected > 0 && isset( $labels[ $selected ] ) ? $labels[ $selected ] : '';
+
+// Render in reverse (5 first, 1 last) so the visual layout — driven by
+// `flex-direction: row-reverse` — can use ~ sibling selectors for the
+// "fill stars 1..N" effect without depending on `:has()`.
+$reversed = array_reverse( $labels, true );
+?>
+<div
+ class="woocommerce-star-rating"
+ role="radiogroup"
+ aria-labelledby="<?php echo esc_attr( $label_id ); ?>"
+ aria-describedby="<?php echo esc_attr( $caption_id ); ?>"
+>
+ <?php foreach ( $reversed as $value => $label ) : ?>
+ <?php
+ $input_id = $id_prefix . '-' . $value;
+ $checked = $value === $selected;
+ ?>
+ <input
+ class="woocommerce-star-rating__input"
+ type="radio"
+ id="<?php echo esc_attr( $input_id ); ?>"
+ name="<?php echo esc_attr( $name ); ?>"
+ value="<?php echo esc_attr( (string) $value ); ?>"
+ data-label="<?php echo esc_attr( $label ); ?>"
+ <?php checked( $checked ); ?>
+ />
+ <label class="woocommerce-star-rating__star" for="<?php echo esc_attr( $input_id ); ?>">
+ <span class="screen-reader-text">
+ <?php
+ printf(
+ /* translators: 1: numeric star rating 2: label text e.g. "Good" */
+ esc_html__( '%1$d out of 5 stars: %2$s', 'woocommerce' ),
+ (int) $value,
+ esc_html( $label )
+ );
+ ?>
+ </span>
+ <svg
+ class="woocommerce-star-rating__icon"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ focusable="false"
+ >
+ <path d="M12 2.5l2.92 6.36 6.99.74-5.21 4.74 1.46 6.86L12 17.77l-6.16 3.43 1.46-6.86L2.09 9.6l6.99-.74L12 2.5z" />
+ </svg>
+ </label>
+ <?php endforeach; ?>
+
+ <span
+ id="<?php echo esc_attr( $caption_id ); ?>"
+ class="woocommerce-star-rating__caption"
+ aria-live="polite"
+ ><?php echo esc_html( $initial_caption ); ?></span>
+</div>
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/StarRatingTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/StarRatingTest.php
new file mode 100644
index 00000000000..58be565de49
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/StarRatingTest.php
@@ -0,0 +1,165 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\OrderReviews;
+
+use Automattic\WooCommerce\Internal\OrderReviews\StarRating;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the accessible star-rating renderer.
+ */
+class StarRatingTest extends WC_Unit_Test_Case {
+
+ /**
+ * Reset filter state between tests.
+ */
+ public function tearDown(): void {
+ remove_all_filters( 'woocommerce_review_order_rating_labels' );
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox render() emits a radiogroup, five radios, and a caption span.
+ */
+ public function test_render_emits_complete_markup(): void {
+ $html = StarRating::render(
+ array(
+ 'name' => 'reviews[42][rating]',
+ 'id_prefix' => 'review-rating-42',
+ 'label_id' => 'review-rating-label-42',
+ )
+ );
+
+ $this->assertStringContainsString( 'role="radiogroup"', $html );
+ $this->assertStringContainsString( 'aria-labelledby="review-rating-label-42"', $html );
+ $this->assertStringContainsString( 'aria-describedby="review-rating-42-caption"', $html );
+
+ // Five inputs, each with a unique id.
+ foreach ( range( 1, 5 ) as $value ) {
+ $this->assertStringContainsString( 'id="review-rating-42-' . $value . '"', $html );
+ $this->assertStringContainsString( 'value="' . $value . '"', $html );
+ }
+
+ $this->assertStringContainsString( 'name="reviews[42][rating]"', $html );
+ $this->assertStringContainsString( 'aria-live="polite"', $html );
+ }
+
+ /**
+ * @testdox render() returns an empty string when required args are missing.
+ */
+ public function test_render_returns_empty_when_required_args_missing(): void {
+ $this->assertSame( '', StarRating::render( array() ) );
+ $this->assertSame(
+ '',
+ StarRating::render(
+ array(
+ 'name' => 'foo',
+ 'id_prefix' => '',
+ 'label_id' => 'bar',
+ )
+ )
+ );
+ }
+
+ /**
+ * @testdox render() pre-checks the supplied selected value.
+ */
+ public function test_render_marks_selected_value_checked(): void {
+ $html = StarRating::render(
+ array(
+ 'name' => 'reviews[42][rating]',
+ 'id_prefix' => 'review-rating-42',
+ 'label_id' => 'review-rating-label-42',
+ 'selected' => 4,
+ )
+ );
+
+ // The `checked()` helper emits `checked='checked'`.
+ $this->assertMatchesRegularExpression(
+ '#id="review-rating-42-4"[^>]*checked#',
+ $html
+ );
+ $this->assertMatchesRegularExpression( '#class="woocommerce-star-rating__caption"[^<]*>\s*Good\s*</span>#s', $html );
+ }
+
+ /**
+ * @testdox render() treats an out-of-range selected value as no selection.
+ */
+ public function test_render_rejects_out_of_range_selected(): void {
+ $args = array(
+ 'name' => 'reviews[42][rating]',
+ 'id_prefix' => 'review-rating-42',
+ 'label_id' => 'review-rating-label-42',
+ );
+
+ $above = StarRating::render( array_merge( $args, array( 'selected' => 99 ) ) );
+ $below = StarRating::render( array_merge( $args, array( 'selected' => -3 ) ) );
+
+ $this->assertDoesNotMatchRegularExpression( '#<input[^>]*\bchecked\b#', $above );
+ $this->assertDoesNotMatchRegularExpression( '#<input[^>]*\bchecked\b#', $below );
+ }
+
+ /**
+ * @testdox get_labels() returns the documented defaults out of the box.
+ */
+ public function test_get_labels_returns_defaults(): void {
+ $labels = StarRating::get_labels();
+
+ $this->assertSame(
+ array(
+ 1 => 'Very poor',
+ 2 => 'Not that bad',
+ 3 => 'Average',
+ 4 => 'Good',
+ 5 => 'Perfect',
+ ),
+ $labels
+ );
+ }
+
+ /**
+ * @testdox woocommerce_review_order_rating_labels filter overrides the defaults.
+ */
+ public function test_filter_can_override_labels(): void {
+ add_filter(
+ 'woocommerce_review_order_rating_labels',
+ static function () {
+ return array(
+ 1 => 'Hated it',
+ 2 => 'Meh',
+ 3 => 'OK',
+ 4 => 'Liked it',
+ 5 => 'Loved it',
+ );
+ }
+ );
+
+ $this->assertSame( 'Loved it', StarRating::get_labels()[5] );
+ }
+
+ /**
+ * @testdox A buggy filter that drops keys falls back to the defaults for the missing slots.
+ */
+ public function test_filter_falls_back_when_keys_missing(): void {
+ add_filter(
+ 'woocommerce_review_order_rating_labels',
+ static function () {
+ // Drop entries 2 and 4 entirely; replace 5.
+ return array(
+ 1 => 'Tiny',
+ 3 => 'Mid',
+ 5 => 'Huge',
+ );
+ }
+ );
+
+ $labels = StarRating::get_labels();
+
+ $this->assertSame( 'Tiny', $labels[1] );
+ $this->assertSame( 'Not that bad', $labels[2] );
+ $this->assertSame( 'Mid', $labels[3] );
+ $this->assertSame( 'Good', $labels[4] );
+ $this->assertSame( 'Huge', $labels[5] );
+ }
+}