Commit ea50b69d580 for woocommerce
commit ea50b69d5807309281a1b1507462232d2db1e9d1
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Wed May 13 18:19:51 2026 +0300
Address Figma UI parity gaps on the Review Order page (#64833)
* Address Figma UI parity gaps on the Review Order page
Walked the Customer Review Request Figma file (8 desktop + mobile
states) against the shipped Review Order page and shipped the
functional parity gaps. Scope is state coverage, not pixel-perfect
parity — the theme owns typography, color, and button styling.
- Reviews-disabled notice surfaced above the form (rendered with the
theme-aware `.woocommerce-info` class) when at least one item is
filtered via `ItemEligibility::STATUS_SKIP`. The dismiss control
toggles a hidden modifier without touching the URL.
- Empty-state template rewritten to "Thank you for your reviews" with
the customer/order meta line above the heading; reviews-summary
paragraph and "Continue shopping" CTA removed (neither is in the
Figma).
- Each row laid out as image / rating / review siblings on desktop
with the existing 600 px breakpoint stacking on mobile.
- Star caption stacks below the stars; row-reverse sibling trick for
filling stars stays inside the inner `__stars` wrapper. Stars sit
flush left with a small consistent gap on the right of each star,
and the global `input[type=radio]+label` theme rule is overridden
locally.
- Submit button gated on the form being dirty: JS tracks initial
rating and text per row via `data-initial-rating` /
`data-initial-text` attributes so a pre-filled row starts disabled
until the customer edits something.
- Inline mandatory-rating validation: a row with typed text and no
rating renders "Please rate this product before submitting your
review." under the stars and blocks submission; the error clears as
soon as a rating is set or the textarea is cleared.
- After any successful submission JS toggles `.is-success` on the
wrapper to hide the form chrome in place and reveal a hidden
"Thank you for your reviews" block. Refresh or re-clicking the email
link still renders the form for any remaining items (server-side
routing unchanged) so there is no URL change and no server-side flag.
- Meta line above the heading uses `.woocommerce-breadcrumb`, dismiss
icon-button no longer carries `wp-element-button`, and hard-coded
link colors on product titles are dropped so the active theme drives
the styling end to end.
* Add changefile(s) from automation for the following project(s): woocommerce
* Trim verbose code comments across Review Order files
* Shorten changelog entry to two lines
* Trim data-initial-text in isRowDirty to match currentText
* Add changefile(s) from automation for the following project(s): woocommerce
* Address Ferdev review: localized validation, meta helper, hook position, mixed-response guard
- Register the `rating_required` validation string in `Endpoint::enqueue_assets()` so the inline error is localizable (no English-only fallback).
- Extract the duplicated meta-line builder out of both templates into `Internal\OrderReviews\Meta::parts_for_order()`.
- Move the `woocommerce_review_order_form_fields` hook out of `__item-review` to a row-level sibling of `__item-row` so it again fires after both columns rather than inside the textarea column.
- Empty-state template `$reviewed_count` docblock now reads "Number of items reviewed" to match what the dispatcher actually counts.
- AJAX submit only swaps to the thank-you view when every processed row succeeded; mixed responses keep the form (and per-row error notes) visible.
- Add three regression tests in `EndpointTest`: disabled-products notice render path, empty-state thank-you markup, and `data-initial-*` row attributes for prefilled rows.
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/64833-wooplug-6687-audit-review-order-page-for-missing-ui-states-vs-figma b/plugins/woocommerce/changelog/64833-wooplug-6687-audit-review-order-page-for-missing-ui-states-vs-figma
new file mode 100644
index 00000000000..8a8a0d5ea35
--- /dev/null
+++ b/plugins/woocommerce/changelog/64833-wooplug-6687-audit-review-order-page-for-missing-ui-states-vs-figma
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Align the Customer Review Order page with the Figma design: ship the functional state gaps, swap to theme-aware classes, and route the customer to a thank-you view in place after any successful submission.
\ 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
index b1a5c792e10..6bb8bb792c3 100644
--- a/plugins/woocommerce/client/legacy/css/order-review.scss
+++ b/plugins/woocommerce/client/legacy/css/order-review.scss
@@ -1,16 +1,17 @@
-/**
- * Review Order page styles.
- *
- * Intentionally minimal. The active theme drives typography, color, and
- * button styling; only the per-item row layout and the interactive
- * star-rating control need bespoke CSS.
- *
- * 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()`.
- */
+// Star DOM order is 5..1 under `flex-direction: row-reverse` so the `~`
+// sibling selector can fill stars 1..N without `:has()`.
.woocommerce-review-order {
+ &.is-success {
+ .woocommerce-review-order__title,
+ .woocommerce-review-order__intro,
+ .woocommerce-review-order__legend,
+ .woocommerce-review-order__notice,
+ .woocommerce-review-order__form {
+ display: none;
+ }
+ }
+
&__items {
list-style: none;
padding: 0;
@@ -27,6 +28,17 @@
}
}
+ &__item-title {
+ margin: 0 0 1em;
+ font-size: 1.25em;
+ line-height: 1.2;
+ font-weight: 500;
+
+ a {
+ text-decoration: underline;
+ }
+ }
+
&__item-row {
display: flex;
gap: 1.5em;
@@ -38,12 +50,21 @@
max-width: 120px;
}
- &__item-fields {
+ &__item-rating {
+ flex: 0 0 auto;
+ min-width: 8em;
+ }
+
+ &__item-rating-label {
+ margin: 0 0 0.25em;
+ }
+
+ &__item-review {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
- gap: 0.75em;
+ gap: 0.5em;
}
&__item-review-textarea {
@@ -52,8 +73,11 @@
resize: vertical;
}
- // Submission status note. Falls back to literal hex if a stylesheet
- // loads before _variables.scss exposes the WC color tokens.
+ &__item-rating-error {
+ margin: 0.5em 0 0;
+ color: var(--wc-red, #a00);
+ }
+
&__item-status {
margin: 0.5em 0 0;
@@ -66,12 +90,41 @@
}
}
- // Locked "Reviewed" row variant: indent the quoted review with a
- // subtle left rule that inherits the theme's text color.
- &__item-reviewed-text {
- margin: 0.5em 0 0;
- padding: 0 0 0 0.75em;
- border-left: 2px solid;
+ // Layered on `.woocommerce-info`; theme paints background, border, icon.
+ &__notice {
+ position: relative;
+
+ &-body {
+ min-width: 0;
+ }
+
+ &-title {
+ margin: 0;
+ font-weight: 600;
+ }
+
+ &-text {
+ margin: 0.25em 0 0;
+ }
+
+ &-dismiss {
+ position: absolute;
+ top: 0.5em;
+ right: 0.5em;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 0;
+ min-width: 24px;
+ min-height: 24px;
+ font-size: 1.25em;
+ line-height: 1;
+ color: inherit;
+ }
+
+ &--hidden {
+ display: none;
+ }
}
@media (max-width: 600px) {
@@ -84,19 +137,31 @@
max-width: 240px;
width: 100%;
}
+
+ &__item-rating,
+ &__item-review {
+ flex: 1 1 auto;
+ width: 100%;
+ }
}
}
.woocommerce-star-rating {
display: inline-flex;
- flex-direction: row-reverse;
- justify-content: flex-end;
- align-items: center;
- flex-wrap: wrap;
- gap: 2px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.25em;
line-height: 1;
- // Visually hide the native radios but keep them in the a11y tree.
+ &__stars {
+ display: inline-flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 4px;
+ }
+
+ // Visually hide radios, keep them in the a11y tree.
&__input {
position: absolute;
width: 1px;
@@ -109,13 +174,19 @@
border: 0;
}
+ // Reset margin so the theme's `input[type=radio]+label` rule can't leak in.
&__star {
cursor: pointer;
display: inline-flex;
- padding: 4px;
+ padding: 0;
+ margin: 0;
color: currentColor;
}
+ &__input + &__star {
+ margin-left: 0;
+ }
+
&__icon {
display: block;
width: 1.5em;
@@ -125,15 +196,12 @@
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).
+ // Checked input fills its own star + every following sibling (visual left).
&__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 {
+ &__stars:hover .woocommerce-star-rating__icon {
opacity: 0.3;
}
@@ -142,21 +210,15 @@
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;
+ font-size: 0.875em;
+ opacity: 0.7;
}
}
diff --git a/plugins/woocommerce/client/legacy/js/frontend/order-review.js b/plugins/woocommerce/client/legacy/js/frontend/order-review.js
index 629e34d1b26..45cf9ad672d 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/order-review.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/order-review.js
@@ -1,14 +1,13 @@
/* 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.
+ * Progressive enhancement for the Review Order page. Without this script the
+ * native radios + form post still work; server validates per row.
*/
( function () {
'use strict';
+ var ERROR_CLASS = 'woocommerce-review-order__item-rating-error';
+
/**
* @param {HTMLElement} container `.woocommerce-star-rating` element.
*/
@@ -37,10 +36,7 @@
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.
+ // DOM order is 5..1; under row-reverse the next visual star is the previous DOM input.
inputs.forEach( function ( input, index ) {
input.addEventListener( 'change', syncCaption );
@@ -73,9 +69,54 @@
syncCaption();
}
+ /**
+ * Return the currently selected rating (1-5) for a row, or 0 if none.
+ *
+ * @param {HTMLElement} row `.woocommerce-review-order__item`
+ * @return {number}
+ */
+ function currentRating( row ) {
+ var checked = row.querySelector(
+ '.woocommerce-star-rating__input:checked'
+ );
+ return checked ? parseInt( checked.value, 10 ) || 0 : 0;
+ }
+
+ /**
+ * Return the current textarea value for a row (trimmed).
+ *
+ * @param {HTMLElement} row `.woocommerce-review-order__item`
+ * @return {string}
+ */
+ function currentText( row ) {
+ var textarea = row.querySelector(
+ '.woocommerce-review-order__item-review-textarea'
+ );
+ return textarea ? ( textarea.value || '' ).trim() : '';
+ }
+
+ /**
+ * Whether a row has been edited since page load.
+ *
+ * @param {HTMLElement} row `.woocommerce-review-order__item`
+ * @return {boolean}
+ */
+ function isRowDirty( row ) {
+ var initialRating = parseInt(
+ row.getAttribute( 'data-initial-rating' ) || '0',
+ 10
+ ) || 0;
+ // Trim to match currentText so prefilled whitespace doesn't mark the row dirty.
+ var initialText = ( row.getAttribute( 'data-initial-text' ) || '' ).trim();
+ return (
+ currentRating( row ) !== initialRating ||
+ currentText( row ) !== initialText
+ );
+ }
+
/**
* Enable / disable the review-order submit button based on whether at
- * least one row has a rating selected.
+ * least one row has been edited since page load.
*
* @param {HTMLFormElement} form `.woocommerce-review-order__form`
*/
@@ -86,53 +127,114 @@
window.console.warn(
'Review Order form is missing its submit button ' +
'(.woocommerce-review-order__submit); ' +
- 'the rating-based gate will not run.'
+ 'the dirty gate will not run.'
);
}
return;
}
+ var rows = Array.prototype.slice.call(
+ form.querySelectorAll( '.woocommerce-review-order__item' )
+ );
+
function syncSubmit() {
- var anyChecked = !! form.querySelector(
- '.woocommerce-star-rating__input:checked'
- );
- submit.disabled = ! anyChecked;
+ submit.disabled = ! rows.some( isRowDirty );
}
- // Expose so initAjaxSubmit can re-run the gate after the request
- // completes (instead of unconditionally enabling the button).
+ // Expose so initAjaxSubmit can re-run the gate after the request completes.
form.syncReviewOrderSubmitGate = syncSubmit;
- form.addEventListener( 'change', function ( event ) {
- if (
- event.target &&
- event.target.classList &&
- event.target.classList.contains(
- 'woocommerce-star-rating__input'
- )
- ) {
- syncSubmit();
+ form.addEventListener( 'change', syncSubmit );
+ form.addEventListener( 'input', syncSubmit );
+
+ syncSubmit();
+ }
+
+ /**
+ * @param {HTMLElement} row `.woocommerce-review-order__item`
+ * @param {boolean} visible Whether the error should be shown.
+ */
+ function setRowRatingError( row, visible ) {
+ var rating = row.querySelector(
+ '.woocommerce-review-order__item-rating'
+ );
+ if ( ! rating ) {
+ return;
+ }
+ var existing = rating.querySelector( '.' + ERROR_CLASS );
+ if ( ! visible ) {
+ if ( existing ) {
+ existing.parentNode.removeChild( existing );
}
+ return;
+ }
+ if ( existing ) {
+ return;
+ }
+ var i18n = ( window.wcOrderReview && window.wcOrderReview.i18n ) || {};
+ var msg =
+ i18n.rating_required ||
+ 'Please rate this product before submitting your review.';
+ var note = document.createElement( 'p' );
+ note.className = ERROR_CLASS;
+ note.setAttribute( 'role', 'alert' );
+ note.textContent = msg;
+ rating.appendChild( note );
+ }
+
+ /**
+ * @param {HTMLFormElement} form `.woocommerce-review-order__form`
+ * @return {function(): boolean} Validator the AJAX submit handler re-runs.
+ */
+ function initRatingValidation( form ) {
+ var rows = Array.prototype.slice.call(
+ form.querySelectorAll( '.woocommerce-review-order__item' )
+ );
+
+ function validate() {
+ var ok = true;
+ rows.forEach( function ( row ) {
+ var needsRating =
+ currentText( row ).length > 0 && currentRating( row ) === 0;
+ setRowRatingError( row, needsRating );
+ if ( needsRating ) {
+ ok = false;
+ }
+ } );
+ return ok;
+ }
+
+ rows.forEach( function ( row ) {
+ row.addEventListener( 'change', function () {
+ if (
+ currentText( row ).length === 0 ||
+ currentRating( row ) > 0
+ ) {
+ setRowRatingError( row, false );
+ }
+ } );
+ row.addEventListener( 'input', function () {
+ if (
+ currentText( row ).length === 0 ||
+ currentRating( row ) > 0
+ ) {
+ setRowRatingError( row, false );
+ }
+ } );
} );
- syncSubmit();
+ return validate;
}
/**
- * Render per-row outcome inside a row's fields container.
+ * Render per-row outcome below the row's columns.
*
* @param {HTMLElement} row `.woocommerce-review-order__item`
* @param {string} status `ok | pending_moderation | error`
* @param {string} [text] Optional message override.
*/
function renderRowStatus( row, status, text ) {
- var fields = row.querySelector(
- '.woocommerce-review-order__item-fields'
- );
- if ( ! fields ) {
- return;
- }
- var existing = fields.querySelector(
+ var existing = row.querySelector(
'.woocommerce-review-order__item-status'
);
if ( existing ) {
@@ -154,15 +256,17 @@
status;
note.setAttribute( 'role', 'status' );
note.textContent = text || defaults[ status ] || defaults.error;
- fields.appendChild( note );
+ row.appendChild( note );
}
/**
* Intercept form submit and POST it to admin-ajax.
*
* @param {HTMLFormElement} form
+ * @param {function(): boolean} validate Returns true when the form is
+ * safe to submit.
*/
- function initAjaxSubmit( form ) {
+ function initAjaxSubmit( form, validate ) {
var ajaxUrl = form.getAttribute( 'data-ajax-url' );
if ( ! ajaxUrl ) {
return;
@@ -171,6 +275,17 @@
form.addEventListener( 'submit', function ( event ) {
event.preventDefault();
+ if ( ! validate() ) {
+ var firstError = form.querySelector( '.' + ERROR_CLASS );
+ if ( firstError && typeof firstError.scrollIntoView === 'function' ) {
+ firstError.scrollIntoView( {
+ behavior: 'smooth',
+ block: 'center',
+ } );
+ }
+ return;
+ }
+
var submit = form.querySelector(
'.woocommerce-review-order__submit'
);
@@ -209,6 +324,8 @@
}
var results = payload.data.results || {};
+ var anySaved = false;
+ var anyFailed = false;
Object.keys( results ).forEach( function ( key ) {
var entry = results[ key ];
var row = form.querySelector(
@@ -219,7 +336,42 @@
if ( row && entry && entry.status ) {
renderRowStatus( row, entry.status );
}
+ if ( ! entry || ! entry.status ) {
+ anyFailed = true;
+ return;
+ }
+ if (
+ entry.status === 'ok' ||
+ entry.status === 'pending_moderation'
+ ) {
+ anySaved = true;
+ } else {
+ anyFailed = true;
+ }
} );
+
+ if ( anySaved && ! anyFailed ) {
+ var wrapper = form.closest(
+ '.woocommerce-review-order'
+ );
+ if ( wrapper ) {
+ wrapper.classList.add( 'is-success' );
+ var success = wrapper.querySelector(
+ '.woocommerce-review-order__success'
+ );
+ if ( success ) {
+ success.hidden = false;
+ }
+ if (
+ typeof wrapper.scrollIntoView === 'function'
+ ) {
+ wrapper.scrollIntoView( {
+ behavior: 'smooth',
+ block: 'start',
+ } );
+ }
+ }
+ }
} )
.catch( function () {
Array.prototype.forEach.call(
@@ -247,6 +399,21 @@
} );
}
+ /**
+ * @param {HTMLElement} notice `.woocommerce-review-order__notice`
+ */
+ function initNoticeDismiss( notice ) {
+ var dismiss = notice.querySelector(
+ '.woocommerce-review-order__notice-dismiss'
+ );
+ if ( ! dismiss ) {
+ return;
+ }
+ dismiss.addEventListener( 'click', function () {
+ notice.classList.add( 'woocommerce-review-order__notice--hidden' );
+ } );
+ }
+
function init() {
var groups = document.querySelectorAll( '.woocommerce-star-rating' );
Array.prototype.forEach.call( groups, initGroup );
@@ -254,8 +421,16 @@
var forms = document.querySelectorAll(
'.woocommerce-review-order__form'
);
- Array.prototype.forEach.call( forms, initSubmitGate );
- Array.prototype.forEach.call( forms, initAjaxSubmit );
+ Array.prototype.forEach.call( forms, function ( form ) {
+ initSubmitGate( form );
+ var validate = initRatingValidation( form );
+ initAjaxSubmit( form, validate );
+ } );
+
+ var notices = document.querySelectorAll(
+ '.woocommerce-review-order__notice'
+ );
+ Array.prototype.forEach.call( notices, initNoticeDismiss );
}
if ( document.readyState === 'loading' ) {
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
index 8d780b945bf..7191d78e22f 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
@@ -597,6 +597,7 @@ class Endpoint {
'ok' => __( 'Thanks, your review is live.', 'woocommerce' ),
'pending_moderation' => __( 'Thanks, your review is pending approval.', 'woocommerce' ),
'error' => __( 'Something went wrong, please try again.', 'woocommerce' ),
+ 'rating_required' => __( 'Please rate this product before submitting your review.', 'woocommerce' ),
),
)
);
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Meta.php b/plugins/woocommerce/src/Internal/OrderReviews/Meta.php
new file mode 100644
index 00000000000..be3fc84755a
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Meta.php
@@ -0,0 +1,52 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\OrderReviews;
+
+use WC_Order;
+
+/**
+ * Shared meta-line helpers for Review Order templates.
+ */
+class Meta {
+
+ /**
+ * Build the meta-line parts shown above the heading on both the form and
+ * empty-state views (customer name, billing email, order #/date).
+ *
+ * @param WC_Order $order Order being reviewed.
+ * @return array<int, string> Non-empty parts ready to be joined with a separator.
+ */
+ public static function parts_for_order( WC_Order $order ): array {
+ $date_created = $order->get_date_created();
+ $customer_name = trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() );
+ $customer_email = $order->get_billing_email();
+ $order_number = $order->get_order_number();
+ $order_date_text = $date_created ? wc_format_datetime( $date_created ) : '';
+
+ if ( '' !== $order_date_text ) {
+ $order_summary = sprintf(
+ /* translators: 1: order number, 2: order date */
+ __( 'Order #%1$s (%2$s)', 'woocommerce' ),
+ $order_number,
+ $order_date_text
+ );
+ } else {
+ $order_summary = sprintf(
+ /* translators: %s: order number */
+ __( 'Order #%s', 'woocommerce' ),
+ $order_number
+ );
+ }
+
+ return array_values(
+ array_filter(
+ array(
+ $customer_name,
+ $customer_email,
+ $order_summary,
+ )
+ )
+ );
+ }
+}
diff --git a/plugins/woocommerce/templates/order/customer-review-order-empty.php b/plugins/woocommerce/templates/order/customer-review-order-empty.php
index cdb9ea72063..3ffe6c7cada 100644
--- a/plugins/woocommerce/templates/order/customer-review-order-empty.php
+++ b/plugins/woocommerce/templates/order/customer-review-order-empty.php
@@ -4,17 +4,12 @@
*
* Theme-overridable. Copy to `yourtheme/woocommerce/order/customer-review-order-empty.php`.
*
- * Rendered when every eligible line item on the order is either already
- * reviewed by the customer or skipped (reviews disabled on the product),
- * so there is nothing left to do on the form.
- *
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 10.8.0
*
- * @var WC_Order $order Order being reviewed.
- * @var int $reviewed_count Number of reviews this customer left on this order.
- * @var float $average_rating Average rating across those reviews (0.0 if none).
+ * @var WC_Order $order Order being reviewed.
+ * @var int $reviewed_count Number of items reviewed on this order.
*/
defined( 'ABSPATH' ) || exit;
@@ -23,71 +18,30 @@ if ( ! $order instanceof WC_Order ) {
return;
}
-// Fall back to the site home if the shop page is missing, mirroring how
-// `Endpoint::gate_request()` handles a missing host page.
-$shop_url = wc_get_page_permalink( 'shop' );
-$cta_url = $shop_url ? $shop_url : home_url( '/' );
+$meta_parts = \Automattic\WooCommerce\Internal\OrderReviews\Meta::parts_for_order( $order );
?>
<div class="woocommerce-review-order woocommerce-review-order--empty">
- <div class="woocommerce-review-order__empty-card">
- <h1 class="woocommerce-review-order__empty-title">
- <?php
- if ( $reviewed_count > 0 ) {
- esc_html_e( 'Thanks for your reviews!', 'woocommerce' );
- } else {
- esc_html_e( 'Nothing to review here', 'woocommerce' );
- }
- ?>
- </h1>
-
- <p class="woocommerce-review-order__empty-body">
- <?php
- if ( $reviewed_count > 0 ) {
- esc_html_e( 'You have nothing left to review on this order. Your feedback helps other shoppers make better decisions.', 'woocommerce' );
- } else {
- esc_html_e( 'There are no products on this order that are open for reviews right now.', 'woocommerce' );
- }
- ?>
- </p>
+ <p class="woocommerce-breadcrumb woocommerce-review-order__meta">
+ <?php echo esc_html( implode( ' · ', $meta_parts ) ); ?>
+ </p>
- <?php if ( $reviewed_count > 0 ) : ?>
- <p class="woocommerce-review-order__empty-summary">
- <?php
- if ( $average_rating > 0 ) {
- $avg = number_format_i18n( $average_rating, 1 );
- /* translators: 1: number of reviews left, 2: average rating with one decimal, e.g. "4.5" */
- $summary_template = _n(
- 'You left %1$d review on this order (average rating %2$s out of 5).',
- 'You left %1$d reviews on this order (average rating %2$s out of 5).',
- (int) $reviewed_count,
- 'woocommerce'
- );
- printf(
- esc_html( $summary_template ),
- (int) $reviewed_count,
- esc_html( $avg )
- );
- } else {
- /* translators: %d: number of reviews left */
- $summary_template = _n(
- 'You left %d review on this order.',
- 'You left %d reviews on this order.',
- (int) $reviewed_count,
- 'woocommerce'
- );
- printf(
- esc_html( $summary_template ),
- (int) $reviewed_count
- );
- }//end if
- ?>
- </p>
- <?php endif; ?>
+ <h1 class="woocommerce-review-order__empty-title">
+ <?php
+ if ( $reviewed_count > 0 ) {
+ esc_html_e( 'Thank you for your reviews', 'woocommerce' );
+ } else {
+ esc_html_e( 'Nothing to review here', 'woocommerce' );
+ }
+ ?>
+ </h1>
- <p class="woocommerce-review-order__empty-actions">
- <a class="button" href="<?php echo esc_url( $cta_url ); ?>">
- <?php esc_html_e( 'Continue shopping', 'woocommerce' ); ?>
- </a>
- </p>
- </div>
+ <p class="woocommerce-review-order__empty-body">
+ <?php
+ if ( $reviewed_count > 0 ) {
+ esc_html_e( 'Your feedback helps other customers make better purchasing decisions.', 'woocommerce' );
+ } else {
+ esc_html_e( 'There are no products on this order that are open for reviews right now.', 'woocommerce' );
+ }
+ ?>
+ </p>
</div>
diff --git a/plugins/woocommerce/templates/order/customer-review-order-row.php b/plugins/woocommerce/templates/order/customer-review-order-row.php
index 16125fd7d02..ef420b2281c 100644
--- a/plugins/woocommerce/templates/order/customer-review-order-row.php
+++ b/plugins/woocommerce/templates/order/customer-review-order-row.php
@@ -4,10 +4,6 @@
*
* Theme-overridable. Copy to `yourtheme/woocommerce/order/customer-review-order-row.php`.
*
- * Renders one product per row: linked title, thumbnail, hidden inputs that
- * tie the submission back to the order item, the accessible star-rating
- * control, and the review textarea.
- *
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 10.8.0
@@ -47,7 +43,12 @@ $rating_control = \Automattic\WooCommerce\Internal\OrderReviews\StarRating::rend
)
);
?>
-<li class="woocommerce-review-order__item" data-row-index="<?php echo esc_attr( (string) $row_index ); ?>">
+<li
+ class="woocommerce-review-order__item"
+ data-row-index="<?php echo esc_attr( (string) $row_index ); ?>"
+ data-initial-rating="<?php echo esc_attr( (string) $existing_rating ); ?>"
+ data-initial-text="<?php echo esc_attr( $existing_text ); ?>"
+>
<p class="woocommerce-review-order__item-title">
<?php if ( $product_link ) : ?>
<a href="<?php echo esc_url( $product_link ); ?>" target="_blank" rel="noopener noreferrer">
@@ -64,53 +65,50 @@ $rating_control = \Automattic\WooCommerce\Internal\OrderReviews\StarRating::rend
<?php echo $image_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_image() returns escaped HTML. ?>
</div>
- <div class="woocommerce-review-order__item-fields">
+ <div class="woocommerce-review-order__item-rating">
<input type="hidden" name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][product_id]" value="<?php echo esc_attr( (string) $product_id ); ?>" />
<input type="hidden" name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][order_item_id]" value="<?php echo esc_attr( (string) $item_id ); ?>" />
- <div class="woocommerce-review-order__item-rating">
- <p id="<?php echo esc_attr( $rating_label_id ); ?>" class="woocommerce-review-order__item-rating-label">
- <?php
- printf(
- '%1$s <span class="required" aria-hidden="true">*</span><span class="screen-reader-text"> %2$s</span>',
- esc_html__( 'Your rating', 'woocommerce' ),
- esc_html__( 'Required', 'woocommerce' )
- );
- ?>
- </p>
- <?php echo $rating_control; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- StarRating::render() returns escaped HTML. ?>
- </div>
-
- <div class="woocommerce-review-order__item-review">
- <label id="<?php echo esc_attr( $review_label_id ); ?>" for="<?php echo esc_attr( $review_input_id ); ?>" class="woocommerce-review-order__item-review-label">
- <?php esc_html_e( 'Your review', 'woocommerce' ); ?>
- </label>
- <textarea
- id="<?php echo esc_attr( $review_input_id ); ?>"
- class="woocommerce-review-order__item-review-textarea"
- name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][text]"
- rows="3"
- placeholder="<?php esc_attr_e( 'Share your experience with this product...', 'woocommerce' ); ?>"
- ><?php echo esc_textarea( $existing_text ); ?></textarea>
- </div>
+ <p id="<?php echo esc_attr( $rating_label_id ); ?>" class="woocommerce-review-order__item-rating-label">
+ <?php
+ printf(
+ '%1$s <span class="required" aria-hidden="true">*</span><span class="screen-reader-text"> %2$s</span>',
+ esc_html__( 'Your rating', 'woocommerce' ),
+ esc_html__( 'Required', 'woocommerce' )
+ );
+ ?>
+ </p>
+ <?php echo $rating_control; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- StarRating::render() returns escaped HTML. ?>
+ </div>
- <?php
- /**
- * Fires after the rating + textarea inside a Review Order form row.
- *
- * Lets extensions inject extra fields (e.g. an "I recommend this"
- * checkbox) without overriding the whole template. Echo HTML directly
- * — the surrounding container expects no return value.
- *
- * @since 10.8.0
- *
- * @param WC_Order_Item_Product $item The line item being reviewed.
- * @param WC_Product $product The associated product.
- * @param WC_Order $order The order.
- * @param int $row_index Zero-based row index for input names.
- */
- do_action( 'woocommerce_review_order_form_fields', $item, $product, $order, $row_index );
- ?>
+ <div class="woocommerce-review-order__item-review">
+ <label id="<?php echo esc_attr( $review_label_id ); ?>" for="<?php echo esc_attr( $review_input_id ); ?>" class="woocommerce-review-order__item-review-label">
+ <?php esc_html_e( 'Your review', 'woocommerce' ); ?>
+ </label>
+ <textarea
+ id="<?php echo esc_attr( $review_input_id ); ?>"
+ class="woocommerce-review-order__item-review-textarea"
+ name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][text]"
+ rows="3"
+ placeholder="<?php esc_attr_e( 'Share your experience with this product...', 'woocommerce' ); ?>"
+ ><?php echo esc_textarea( $existing_text ); ?></textarea>
</div>
</div>
+
+ <?php
+ /**
+ * Fires after the rating + textarea inside a Review Order form row, as a
+ * sibling of the row's columns so injected fields render below them.
+ *
+ * Echo HTML directly; the surrounding container expects no return value.
+ *
+ * @since 10.8.0
+ *
+ * @param WC_Order_Item_Product $item The line item being reviewed.
+ * @param WC_Product $product The associated product.
+ * @param WC_Order $order The order.
+ * @param int $row_index Zero-based row index for input names.
+ */
+ do_action( 'woocommerce_review_order_form_fields', $item, $product, $order, $row_index );
+ ?>
</li>
diff --git a/plugins/woocommerce/templates/order/customer-review-order.php b/plugins/woocommerce/templates/order/customer-review-order.php
index 3b832e048ba..27d868c6ff1 100644
--- a/plugins/woocommerce/templates/order/customer-review-order.php
+++ b/plugins/woocommerce/templates/order/customer-review-order.php
@@ -1,18 +1,8 @@
<?php
/**
- * Customer Review Order page
+ * Customer Review Order page.
*
- * Read-only landing page surfaced from the Customer Review Request email.
- * Lists the eligible line items from a completed order so the customer can
- * review what they purchased.
- *
- * This template can be overridden by copying it to yourtheme/woocommerce/order/customer-review-order.php.
- *
- * HOWEVER, on occasion WooCommerce will need to update template files and you
- * (the theme developer) will need to copy the new files to your theme to
- * maintain compatibility. We try to do this as little as possible, but it does
- * happen. When this occurs the version of the template file will be bumped and
- * the readme will list any important changes.
+ * Theme-overridable. Copy to `yourtheme/woocommerce/order/customer-review-order.php`.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
@@ -27,34 +17,7 @@ if ( ! $order instanceof WC_Order ) {
return;
}
-$date_created = $order->get_date_created();
-$customer_name = trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() );
-$customer_email = $order->get_billing_email();
-$order_number = $order->get_order_number();
-$order_date_text = $date_created ? wc_format_datetime( $date_created ) : '';
-
-if ( '' !== $order_date_text ) {
- $order_summary = sprintf(
- /* translators: 1: order number, 2: order date */
- __( 'Order #%1$s (%2$s)', 'woocommerce' ),
- $order_number,
- $order_date_text
- );
-} else {
- $order_summary = sprintf(
- /* translators: %s: order number */
- __( 'Order #%s', 'woocommerce' ),
- $order_number
- );
-}
-
-$meta_parts = array_filter(
- array(
- $customer_name,
- $customer_email,
- $order_summary,
- )
-);
+$meta_parts = \Automattic\WooCommerce\Internal\OrderReviews\Meta::parts_for_order( $order );
/**
* Filter the eligible items rendered on the Review Order page.
@@ -69,16 +32,13 @@ $meta_parts = array_filter(
*/
$items = (array) apply_filters( 'woocommerce_review_order_eligible_items', $order->get_items(), $order );
-// Single batched lookup of every existing review by this customer for the
-// items below. Without this each decide() call would issue its own query.
+// Batched lookup; without this each decide() call would issue its own query.
\Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility::preload_for_items( $items, $order );
-// Pre-compute one decision per item so we know whether to render the form
-// (any item still missing a review for this order) or fall through to the
-// empty-state thank-you (every renderable item already has a review tied
-// to this order).
+// Skipped rows are counted so the disabled-products notice can render above the form.
$decisions = array();
$has_unreviewed_row = false;
+$skipped_count = 0;
foreach ( $items as $item ) {
if ( ! $item instanceof WC_Order_Item_Product ) {
continue;
@@ -90,6 +50,7 @@ foreach ( $items as $item ) {
$decision = \Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility::decide( $item, $order );
if ( \Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility::STATUS_SKIP === $decision['status'] ) {
+ ++$skipped_count;
continue;
}
@@ -104,72 +65,30 @@ foreach ( $items as $item ) {
);
}//end foreach
-// Empty-state: no actionable rows remain. The Endpoint already stamped the
-// completion meta before we got here, so this branch is purely the view.
+// Empty-state: no actionable rows remain.
if ( ! $has_unreviewed_row ) {
$reviewed_count = 0;
- $rating_total = 0;
- $rating_n = 0;
-
- if ( '' !== $customer_email ) {
- $comment_ids = array();
- foreach ( $decisions as $entry ) {
- $existing_review = $entry['decision']['comment'] ?? null;
- if ( $existing_review instanceof WP_Comment ) {
- $comment_ids[] = (int) $existing_review->comment_ID;
- }
- }
- if ( ! empty( $comment_ids ) ) {
- update_meta_cache( 'comment', $comment_ids );
- }
-
- // Multiple line items can map to the same review (same parent
- // product on different variations or quantity-split lines). Count
- // each underlying comment once so the customer-facing summary
- // matches what they actually wrote.
- $counted = array();
- foreach ( $decisions as $entry ) {
- $existing_review = $entry['decision']['comment'] ?? null;
- if ( ! $existing_review instanceof WP_Comment ) {
- continue;
- }
- $cid = (int) $existing_review->comment_ID;
- if ( isset( $counted[ $cid ] ) ) {
- continue;
- }
- $counted[ $cid ] = true;
+ foreach ( $decisions as $entry ) {
+ if ( $entry['decision']['comment'] instanceof WP_Comment ) {
++$reviewed_count;
- $rating = (int) get_comment_meta( $cid, 'rating', true );
- if ( $rating > 0 ) {
- $rating_total += $rating;
- ++$rating_n;
- }
- }//end foreach
- }//end if
-
- $average_rating = $rating_n > 0 ? round( $rating_total / $rating_n, 1 ) : 0.0;
+ }
+ }
wc_get_template(
'order/customer-review-order-empty.php',
array(
'order' => $order,
'reviewed_count' => $reviewed_count,
- 'average_rating' => $average_rating,
)
);
return;
}//end if
-// Single batched lookup of every existing review by this customer for the
-// items below. Without this each decide() call would issue its own query.
-\Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility::preload_for_items( $items, $order );
-
-// The Endpoint has already validated the URL key against the order key, so the
-// canonical value on the order is the right thing to echo into the form post.
-$order_key = (string) $order->get_order_key();
+$order_key = (string) $order->get_order_key();
+$wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '';
?>
<div class="woocommerce-review-order">
- <p class="woocommerce-review-order__meta">
+ <p class="woocommerce-breadcrumb woocommerce-review-order__meta">
<?php echo esc_html( implode( ' · ', $meta_parts ) ); ?>
</p>
@@ -185,6 +104,29 @@ $order_key = (string) $order->get_order_key();
<?php esc_html_e( '* Mandatory fields', 'woocommerce' ); ?>
</p>
+ <?php if ( $skipped_count > 0 ) : ?>
+ <div
+ class="woocommerce-info woocommerce-review-order__notice"
+ role="status"
+ >
+ <div class="woocommerce-review-order__notice-body">
+ <p class="woocommerce-review-order__notice-title">
+ <?php esc_html_e( "Don't see all your products?", 'woocommerce' ); ?>
+ </p>
+ <p class="woocommerce-review-order__notice-text">
+ <?php esc_html_e( 'Some products may not be available for review because the store has disabled reviews for them.', 'woocommerce' ); ?>
+ </p>
+ </div>
+ <button
+ type="button"
+ class="woocommerce-review-order__notice-dismiss"
+ aria-label="<?php esc_attr_e( 'Dismiss this notice', 'woocommerce' ); ?>"
+ >
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <?php endif; ?>
+
<form
class="woocommerce-review-order__form"
method="post"
@@ -226,10 +168,19 @@ $order_key = (string) $order->get_order_key();
<div class="woocommerce-review-order__actions">
<button
type="submit"
- class="woocommerce-review-order__submit button"
+ class="woocommerce-review-order__submit button<?php echo esc_attr( $wp_button_class ); ?>"
>
<?php esc_html_e( 'Submit reviews', 'woocommerce' ); ?>
</button>
</div>
</form>
+
+ <div class="woocommerce-review-order__success" hidden>
+ <h1 class="woocommerce-review-order__empty-title">
+ <?php esc_html_e( 'Thank you for your reviews', 'woocommerce' ); ?>
+ </h1>
+ <p class="woocommerce-review-order__empty-body">
+ <?php esc_html_e( 'Your feedback helps other customers make better purchasing decisions.', 'woocommerce' ); ?>
+ </p>
+ </div>
</div>
diff --git a/plugins/woocommerce/templates/order/star-rating.php b/plugins/woocommerce/templates/order/star-rating.php
index 6941b759054..a82d4cba042 100644
--- a/plugins/woocommerce/templates/order/star-rating.php
+++ b/plugins/woocommerce/templates/order/star-rating.php
@@ -4,12 +4,6 @@
*
* 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
@@ -26,9 +20,7 @@ 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()`.
+// Reverse so row-reverse + `~` selectors can fill stars 1..N without `:has()`.
$reversed = array_reverse( $labels, true );
?>
<div
@@ -37,43 +29,45 @@ $reversed = array_reverse( $labels, true );
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; ?>
+ <div class="woocommerce-star-rating__stars">
+ <?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; ?>
+ </div>
<span
id="<?php echo esc_attr( $caption_id ); ?>"
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
index 31c7545d821..bef8c22dd48 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
@@ -506,6 +506,110 @@ class EndpointTest extends WC_Unit_Test_Case {
$this->assertEmpty( $fresh->get_meta( SubmissionHandler::COMPLETED_META_KEY ) );
}
+ /**
+ * @testdox The disabled-products info notice renders when at least one order item is STATUS_SKIP and the form is still active.
+ */
+ public function test_disabled_products_notice_renders_above_form(): void {
+ $order = OrderHelper::create_order();
+ $reviewable = WC_Helper_Product::create_simple_product();
+ $disabled = WC_Helper_Product::create_simple_product();
+ wp_update_post(
+ array(
+ 'ID' => $disabled->get_id(),
+ 'comment_status' => 'closed',
+ )
+ );
+ $order->set_status( OrderStatus::COMPLETED );
+ foreach ( $order->get_items() as $item ) {
+ $order->remove_item( $item->get_id() );
+ }
+ $order->add_product( $reviewable, 1 );
+ $order->add_product( $disabled, 1 );
+ $order->save();
+
+ $_GET = array( 'key' => $order->get_order_key() );
+
+ $html = $this->render( $order->get_id() );
+
+ $this->assertStringContainsString( 'woocommerce-info woocommerce-review-order__notice', $html );
+ $this->assertStringContainsString( 'see all your products?', $html );
+ $this->assertStringContainsString( 'woocommerce-review-order__form', $html );
+ }
+
+ /**
+ * @testdox The empty-state thank-you template renders the meta line, heading, and body when no actionable rows remain.
+ */
+ public function test_empty_state_template_renders_meta_and_thank_you(): void {
+ $order = OrderHelper::create_order();
+ $product = WC_Helper_Product::create_simple_product();
+ $order->set_billing_email( 'thanks@example.test' );
+ $order->set_status( OrderStatus::COMPLETED );
+ foreach ( $order->get_items() as $item ) {
+ $order->remove_item( $item->get_id() );
+ }
+ $order->add_product( $product, 1 );
+ $order->save();
+
+ $comment_id = (int) wp_insert_comment(
+ array(
+ 'comment_post_ID' => $product->get_id(),
+ 'comment_author' => 'Thanks',
+ 'comment_author_email' => 'thanks@example.test',
+ 'comment_content' => 'Loved it.',
+ 'comment_type' => 'review',
+ 'comment_approved' => 1,
+ )
+ );
+ add_comment_meta( $comment_id, ItemEligibility::ORDER_META_KEY, (int) $order->get_id(), true );
+
+ $_GET = array( 'key' => $order->get_order_key() );
+
+ $html = $this->render( $order->get_id() );
+
+ $this->assertStringContainsString( 'woocommerce-review-order--empty', $html );
+ $this->assertStringContainsString( 'woocommerce-breadcrumb woocommerce-review-order__meta', $html );
+ $this->assertStringContainsString( 'Order #' . $order->get_order_number(), $html );
+ $this->assertStringContainsString( 'Thank you for your reviews', $html );
+ $this->assertStringContainsString( 'Your feedback helps', $html );
+ }
+
+ /**
+ * @testdox A pre-filled row exposes the existing rating and text via data-initial-* attributes so the JS dirty gate can detect edits.
+ */
+ public function test_row_exposes_data_initial_attributes_for_prefilled_review(): void {
+ $order = OrderHelper::create_order();
+ $reviewed = WC_Helper_Product::create_simple_product();
+ $unreviewed = WC_Helper_Product::create_simple_product();
+ $order->set_billing_email( 'prefill@example.test' );
+ $order->set_status( OrderStatus::COMPLETED );
+ foreach ( $order->get_items() as $item ) {
+ $order->remove_item( $item->get_id() );
+ }
+ $order->add_product( $reviewed, 1 );
+ $order->add_product( $unreviewed, 1 );
+ $order->save();
+
+ $comment_id = (int) wp_insert_comment(
+ array(
+ 'comment_post_ID' => $reviewed->get_id(),
+ 'comment_author' => 'Prefill',
+ 'comment_author_email' => 'prefill@example.test',
+ 'comment_content' => 'Solid four.',
+ 'comment_type' => 'review',
+ 'comment_approved' => 1,
+ )
+ );
+ add_comment_meta( $comment_id, 'rating', 4, true );
+ add_comment_meta( $comment_id, ItemEligibility::ORDER_META_KEY, (int) $order->get_id(), true );
+
+ $_GET = array( 'key' => $order->get_order_key() );
+
+ $html = $this->render( $order->get_id() );
+
+ $this->assertStringContainsString( 'data-initial-rating="4"', $html );
+ $this->assertStringContainsString( 'data-initial-text="Solid four."', $html );
+ }
+
/**
* @testdox The completed-at meta is never overwritten on subsequent loads.
*/