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">&times;</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.
 	 */