Commit c8848636592 for woocommerce

commit c8848636592076bf83da4b42def272e38c4d48af
Author: Ahmed <ahmed.el.azzabi@automattic.com>
Date:   Wed Sep 16 21:00:02 2026 +0100

    Add "reviews per page" screen option to Product Reviews list (#66680)

    * Add "reviews per page" screen option to Product Reviews list table

    The Products > Reviews list table already reads the per-page value from
    the `edit_comments_per_page` user option, but never registered a
    `per_page` screen option, so the "Items per page" control was missing
    from the Screen Options tab. Register it on the reviews screen load hook
    so merchants can adjust how many reviews are shown per page.

    Closes #55944

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Use a dedicated per-page option for Product Reviews screen

    Align with the other WooCommerce list-table screens (Orders, Approved
    Download Directories, Stock Notifications), which each register their own
    dedicated `per_page` user option and a save filter rather than reusing a
    core option. Introduce `ReviewsListTable::PER_PAGE_USER_OPTION_KEY`
    (`edit_product_reviews_per_page`) so the reviews "per page" setting is
    independent from the WordPress Comments screen, and read it from
    `get_per_page()`.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Fix return-type colon spacing in reviews screen option tests

    phpcs (PSR12.Functions.ReturnTypeDeclaration.SpaceBeforeColon) flags the
    newly added test methods, which must use `(): void` without a space
    before the colon.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Fix "Class WP_List_Table not found" fatal from reviews per-page option

    The PHP unit suite failed at bootstrap with `Class "WP_List_Table" not
    found`. The `Reviews` service is instantiated by the DI container on
    every load (including the non-admin PHPUnit bootstrap), and its
    constructor registered the screen-option save filter using
    `ReviewsListTable::PER_PAGE_USER_OPTION_KEY`. Referencing that class
    constant made the Jetpack autoloader load `ReviewsListTable`, which
    `extends WP_List_Table` — an admin-only core class that is only included
    on admin screen requests. Outside admin context the parent class is
    undefined, so autoloading fataled and aborted the whole test run.

    Move the option-key constant to `Reviews` (which is always loaded and
    does not extend `WP_List_Table`) so the constructor can build the
    `set_screen_option_*` hook name without pulling in the list table.
    `ReviewsListTable::get_per_page()` now reads the key from
    `Reviews::PER_PAGE_USER_OPTION_KEY`; that reference only runs on the
    admin reviews screen, where `WP_List_Table` is already loaded. Behavior
    and the stored option name (`edit_product_reviews_per_page`) are
    unchanged; the constant was introduced in this unreleased branch, so
    relocating it is not a backward-compatibility concern.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Address review: legacy filter bridge and per-page bounds

    Two changes from PR review on the reviews per-page option:

    1. Preserve the legacy `edit_comments_per_page` behavior on the Product
       Reviews screen. Moving the list to the dedicated
       `edit_product_reviews_per_page` option changed the filter name fired by
       WP_List_Table::get_items_per_page(), which removed
       `edit_comments_per_page` from the reviews execution path and would have
       broken existing integrations (including the workaround in #55944).
       `get_per_page()` now bridges the legacy filter via
       apply_filters_deprecated(), scoped to this screen (the method only runs
       while preparing the reviews list). The legacy filter is applied last so
       it keeps precedence over the dedicated filter during the deprecation
       window. Added a regression test asserting that precedence.

    2. Reapply WordPress core's 1-999 per-page bounds in
       set_reviews_per_page_option(). Custom screen-option keys bypass the
       validation core applies in set_screen_options(), so absint() alone let
       an out-of-range value reach the get_comments() query size. Out-of-range
       values are now rejected (nothing is saved), matching core. Added
       boundary coverage for 0, negative, 1, 999 and values above 999.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Use a plain compatibility shim for edit_comments_per_page (no deprecation)

    `edit_comments_per_page` is a WordPress core filter, so WooCommerce has no
    standing to deprecate it. Replace the earlier apply_filters_deprecated()
    bridge (which would have emitted a misleading "core hook deprecated in
    WooCommerce 11.1.0" notice and misattributed ownership) with a plain
    apply_filters() compatibility shim, mirroring how this file already
    re-applies the core `comment_row_actions` filter.

    Behavior is unchanged: the dedicated `edit_product_reviews_per_page`
    option/filter remains the primary control, and the legacy
    `edit_comments_per_page` filter is still applied last (retaining
    precedence) so existing integrations — including the workaround from
    issue #55944 — keep working. Dropped the `setExpectedDeprecated()` call
    from the regression test accordingly.

    The 1-999 per-page bounds check is intentionally kept: WordPress core only
    applies those bounds to its own enumerated per-page options, not to custom
    keys like `edit_product_reviews_per_page`, which fall through to the
    default branch of set_screen_options() with no validation.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Align closure assignments in reviews per-page test

    Fixes the phpcs Generic.Formatting.MultipleStatementAlignment warning (CI
    treats warnings as failures) on the consecutive closure assignments added
    for the legacy-filter compatibility test.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Improve test isolation for reviews per-page tests

    Follow-ups from PR review (test-only, no production changes):

    - Exercise screen-option registration through load_reviews_screen()
      instead of calling add_screen_options() directly, so removing the
      registration call from the production wiring would fail the test.
    - Snapshot and restore the global $current_screen with try/finally around
      set_current_screen(), which mutates shared test state.
    - Remove the filters added by the list-table precedence test in a finally
      block, so a failed assertion cannot leak them into later tests.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * Update comment blocks

    * Keep reviews list and Screen Options input in sync (align with WP core)

    Per review feedback (Aljullu): match how WordPress core handles a
    per-page filter — the filter keeps precedence, and the Screen Options
    input reflects the effective value so it never silently disagrees with
    the list.

    Previously the legacy `edit_comments_per_page` filter was applied
    manually inside get_per_page(), i.e. only on the list path. WP core's
    render_per_page_options() draws the input value by applying the option's
    own filter (edit_product_reviews_per_page), so it never saw our manual
    re-application, and the input could show a different number than the
    list (reported by oaratovskyi).

    Route the legacy filter through the dedicated hook instead: register an
    edit_product_reviews_per_page callback that re-applies
    edit_comments_per_page. Now both the list (get_items_per_page) and the
    input (render_per_page_options) run the same filter chain, so they always
    agree, the legacy filter still wins (BC preserved), and behaviour matches
    core.

    Also addresses jorgeatorres's docblock feedback: get_per_page() collapses
    to one line, removing the duplicated backward-compatibility narrative and
    the misplaced hook docblock that sat over a non-apply_filters() line. The
    edit_comments_per_page re-application is documented once, where it lives,
    without tagging a WordPress-core hook with a misleading WooCommerce
    @since. Tests updated to assert the list and input stay in sync.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JMy7Y7qJHtEEyQ57Ae3aik

    * test: fix phpcs CommentHooks on reviews per-page bridge assertion

    The `apply_filters( Reviews::PER_PAGE_USER_OPTION_KEY, 20 )` call in the
    bridge test mirrors the filter WordPress core applies when rendering the
    Screen Options input; it is not a WooCommerce-owned hook. Suppress the
    whole WooCommerce.Commenting.CommentHooks sniff on that line (rather than
    a single sub-code) so it no longer trips HookCommentWrongStyle in the
    Lint CI job.

    * chore: Simplify reviews per-page compatibility comments

    The compatibility bridge accumulated the same implementation rationale
    across its registration, callback, list-table getter, and tests.

    Keep only the non-obvious architectural and backward-compatibility notes,
    while retaining the targeted PHPCS suppression for the WordPress filter
    invocation exercised by the regression test.

    * fix: Harden product reviews per-page validation

    Invalid values previously returned the incoming filter status. An earlier callback could therefore make an out-of-range Screen Options value persist.

    Reject invalid values unconditionally and cover the dynamic save hook and dedicated user-option lookup so regressions fail at the integration points.

    * fix: Restore reviews screen state after test

    The screen-option registration test left the shared Reviews service with an initialized list table. This made the result depend on the test execution order.

    Restore the previous list-table value in the existing finally block. The tests are now independent, with no change to production behavior.

    Refs #55944

    * fix: Support PHP 7.4 in reviews screen test

    The state-restoration test read a protected property without first making it accessible. PHP 7.4 rejects that access, which caused both PHP 7.4 CI jobs to fail.

    Mark the reflection property as accessible before reading and restoring it. This keeps the test compatible with PHP 7.4 and newer versions, with no production-code change.

    Refs #55944

    * refactor: Finalize reviews screen option API

    Product Reviews gains a dedicated Screen Options key in WooCommerce 11.2.

    The helper exposed more visibility than its only caller needs. The new symbols also named the shipped 11.1 release.

    Limit helper visibility, mark the key internal, then record the correct introduction version.

    Refs #55944

    * test: Reuse the registered reviews handler

    Product review tests should exercise the bootstrap container instance.

    Constructing a second Reviews object registers the screen-option filter twice. Removing that callback also obscures the production wiring.

    Resolve the shared handler from the container. Let parent hook restoration own its lifecycle.

    Refs #55944

    * test: Rely on inherited reviews cleanup

    WC_Unit_Test_Case rolls back user metadata, then resets the current user after every test.

    The dedicated-option test duplicated that cleanup in a finally block.

    Rely on inherited isolation so the test only describes behavior.

    Refs #55944

    * docs: Update Product Reviews docblocks

    The screen option code will ship in WooCommerce 11.3.0, while its docblocks still referenced 11.2.0. The private helper also retained public API tags.\n\nUse 11.3.0 for public additions. Keep the private helper docblock limited to its behavior.

    * docs: Clarify Product Reviews per-page compatibility

    Product Reviews keeps a separate saved per-page value while still applying the legacy Comments filter. The filter bridge can look like it also shares the saved Comments setting.

    Explain that WordPress uses the option key as a filter name and that only callbacks remain shared.

    Refs #55944

    * fix: Resolve product reviews per-page filter precedence

    * test: Verify reviews per-page filters through public hook

    ---------

    Co-authored-by: Claude <noreply@anthropic.com>
    Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
    Co-authored-by: oaratovskyi <oleksandr.aratovskyi@automattic.com>

diff --git a/plugins/woocommerce/changelog/55944-add-product-reviews-per-page-screen-option b/plugins/woocommerce/changelog/55944-add-product-reviews-per-page-screen-option
new file mode 100644
index 00000000000..b52780615b0
--- /dev/null
+++ b/plugins/woocommerce/changelog/55944-add-product-reviews-per-page-screen-option
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Add a "Number of reviews per page" control to the Screen Options tab on the Products > Reviews list table, letting merchants adjust how many product reviews are shown per page.
diff --git a/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php b/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php
index 29d4fae0553..9b61688086d 100644
--- a/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php
+++ b/plugins/woocommerce/src/Internal/Admin/ProductReviews/Reviews.php
@@ -19,6 +19,19 @@ class Reviews {
 	 */
 	const MENU_SLUG = 'product-reviews';

+	/**
+	 * User option key that stores the number of reviews to display per page on the Product Reviews screen.
+	 *
+	 * Kept here to avoid autoloading the admin-only {@see ReviewsListTable} outside admin requests.
+	 *
+	 * @since 11.3.0
+	 *
+	 * @var string
+	 *
+	 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
+	 */
+	const PER_PAGE_USER_OPTION_KEY = 'edit_product_reviews_per_page';
+
 	/**
 	 * Reviews page hook name.
 	 *
@@ -47,6 +60,10 @@ class Reviews {

 		add_filter( 'parent_file', array( $this, 'edit_review_parent_file' ) );
 		add_action( 'admin_notices', array( $this, 'display_notices' ) );
+
+		add_filter( 'set_screen_option_' . self::PER_PAGE_USER_OPTION_KEY, array( $this, 'set_reviews_per_page_option' ), 10, 3 );
+		// WordPress also uses the option key as a filter name. Bridge legacy callbacks first so the dedicated filter can override them.
+		add_filter( self::PER_PAGE_USER_OPTION_KEY, array( $this, 'apply_legacy_reviews_per_page_filter' ), 0 );
 	}

 	/**
@@ -556,10 +573,68 @@ class Reviews {
 	 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
 	 */
 	public function load_reviews_screen(): void {
+		$this->add_screen_options();
 		$this->reviews_list_table = $this->make_reviews_list_table();
 		$this->reviews_list_table->process_bulk_action();
 	}

+	/**
+	 * Registers the screen options for the Reviews page.
+	 *
+	 * @return void
+	 */
+	private function add_screen_options(): void {
+		add_screen_option(
+			'per_page',
+			array(
+				'label'   => __( 'Number of reviews per page:', 'woocommerce' ),
+				'default' => 20,
+				'option'  => self::PER_PAGE_USER_OPTION_KEY,
+			)
+		);
+	}
+
+	/**
+	 * Saves the reviews per-page screen option within WordPress's standard `1`–`999` bounds.
+	 *
+	 * @since 11.3.0
+	 *
+	 * @param mixed  $screen_option The value to save instead of the option value. Default false (to skip saving the current option).
+	 * @param string $option        The option name.
+	 * @param int    $value         The number of reviews to show per page.
+	 * @return mixed
+	 *
+	 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
+	 */
+	public function set_reviews_per_page_option( $screen_option, $option, $value ) {
+		if ( self::PER_PAGE_USER_OPTION_KEY !== $option ) {
+			return $screen_option;
+		}
+
+		$value = (int) $value;
+
+		if ( $value < 1 || $value > 999 ) {
+			return false;
+		}
+
+		return $value;
+	}
+
+	/**
+	 * Applies the legacy `edit_comments_per_page` filter to the dedicated reviews per-page value.
+	 *
+	 * @since 11.3.0
+	 *
+	 * @param int $per_page Number of reviews to show per page.
+	 * @return int
+	 *
+	 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
+	 */
+	public function apply_legacy_reviews_per_page_filter( $per_page ): int {
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Core hook retained for compatibility.
+		return (int) apply_filters( 'edit_comments_per_page', $per_page );
+	}
+
 	/**
 	 * Renders the Reviews page.
 	 *
diff --git a/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsListTable.php b/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsListTable.php
index 3e9f7b74974..dcda375f788 100644
--- a/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsListTable.php
+++ b/plugins/woocommerce/src/Internal/Admin/ProductReviews/ReviewsListTable.php
@@ -124,7 +124,7 @@ class ReviewsListTable extends WP_List_Table {
 	 * @return int Customized per-page value if available, or 20 as the default.
 	 */
 	protected function get_per_page() : int {
-		return $this->get_items_per_page( 'edit_comments_per_page' );
+		return $this->get_items_per_page( Reviews::PER_PAGE_USER_OPTION_KEY );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsListTableTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsListTableTest.php
index a224f24fed0..84fdfa5d9c8 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsListTableTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsListTableTest.php
@@ -4,6 +4,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\Tests\Internal\Admin\ProductReviews;

+use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
 use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsListTable;
 use DOMDocument;
 use DOMElement;
@@ -2009,6 +2010,60 @@ class ReviewsListTableTest extends WC_Unit_Test_Case {
 		$this->assertSame( 20, $method->invoke( $list_table ) );
 	}

+	/**
+	 * @testdox `get_per_page` uses the dedicated Product Reviews option instead of the Comments option.
+	 *
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsListTable::get_per_page()
+	 *
+	 * @return void
+	 * @throws ReflectionException If the method doesn't exist.
+	 */
+	public function test_get_per_page_uses_dedicated_user_option(): void {
+		$user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
+
+		wp_set_current_user( $user_id );
+		update_user_option( $user_id, Reviews::PER_PAGE_USER_OPTION_KEY, 55 );
+		update_user_option( $user_id, 'edit_comments_per_page', 35 );
+
+		$list_table = $this->get_reviews_list_table();
+		$method     = ( new ReflectionClass( $list_table ) )->getMethod( 'get_per_page' );
+		$method->setAccessible( true );
+
+		$this->assertSame( 55, $method->invoke( $list_table ) );
+	}
+
+	/**
+	 * @testdox `get_per_page` and the Screen Options input use the legacy `edit_comments_per_page` value.
+	 *
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsListTable::get_per_page()
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::apply_legacy_reviews_per_page_filter()
+	 *
+	 * @return void
+	 * @throws ReflectionException If the method doesn't exist.
+	 */
+	public function test_get_per_page_bridges_legacy_comments_filter(): void {
+		wc_get_container()->get( Reviews::class );
+
+		$list_table = $this->get_reviews_list_table();
+		$method     = ( new ReflectionClass( $list_table ) )->getMethod( 'get_per_page' );
+		$method->setAccessible( true );
+
+		$legacy = static function () {
+			return 45;
+		};
+		add_filter( 'edit_comments_per_page', $legacy );
+
+		try {
+			$this->assertSame( 45, $method->invoke( $list_table ) );
+
+			// phpcs:ignore WooCommerce.Commenting.CommentHooks -- Mirrors WordPress's Screen Options filter.
+			$input_per_page = (int) apply_filters( Reviews::PER_PAGE_USER_OPTION_KEY, 20 );
+			$this->assertSame( 45, $input_per_page );
+		} finally {
+			remove_filter( 'edit_comments_per_page', $legacy );
+		}
+	}
+
 	/**
 	 * @testdox `comments_bubble` displays a bubble with information about pending and approved reviews for the corresponding product.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsTest.php
index 03cd4613ca8..1039ba0f047 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/ProductReviews/ReviewsTest.php
@@ -92,6 +92,120 @@ class ReviewsTest extends WC_Unit_Test_Case {
 		$this->assertInstanceOf( ReviewsListTable::class, $list_table_property->getValue( $reviews ) );
 	}

+	/**
+	 * @testdox `load_reviews_screen` registers the "per page" screen option bound to the dedicated reviews user option.
+	 *
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::load_reviews_screen()
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::add_screen_options()
+	 *
+	 * @return void
+	 * @throws ReflectionException If the property is not found.
+	 */
+	public function test_load_reviews_screen_registers_per_page_option(): void {
+		global $current_screen;
+
+		$previous_screen     = $current_screen;
+		$reviews             = wc_get_container()->get( Reviews::class );
+		$list_table_property = ( new ReflectionClass( $reviews ) )->getProperty( 'reviews_list_table' );
+		$list_table_property->setAccessible( true );
+		$previous_list_table = $list_table_property->getValue( $reviews );
+
+		try {
+			set_current_screen( 'product_page_product-reviews' );
+
+			// Exercise the production wiring: registration must happen through `load_reviews_screen()`, so that
+			// dropping the `add_screen_options()` call from it would fail this test.
+			$reviews->load_reviews_screen();
+
+			$option = get_current_screen()->get_option( 'per_page' );
+
+			$this->assertIsArray( $option );
+			$this->assertArrayHasKey( 'option', $option );
+			$this->assertSame( Reviews::PER_PAGE_USER_OPTION_KEY, $option['option'] );
+			$this->assertArrayHasKey( 'default', $option );
+			$this->assertSame( 20, $option['default'] );
+		} finally {
+			$list_table_property->setValue( $reviews, $previous_list_table );
+			$current_screen = $previous_screen; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		}
+	}
+
+	/**
+	 * @testdox `set_reviews_per_page_option` saves values within the 1–999 bounds and rejects everything else.
+	 *
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::set_reviews_per_page_option()
+	 *
+	 * @return void
+	 */
+	public function test_set_reviews_per_page_option(): void {
+		$reviews = wc_get_container()->get( Reviews::class );
+
+		// Values inside the 1–999 range are saved.
+		$this->assertSame( 1, $reviews->set_reviews_per_page_option( false, Reviews::PER_PAGE_USER_OPTION_KEY, 1 ) );
+		$this->assertSame( 55, $reviews->set_reviews_per_page_option( false, Reviews::PER_PAGE_USER_OPTION_KEY, 55 ) );
+		$this->assertSame( 999, $reviews->set_reviews_per_page_option( false, Reviews::PER_PAGE_USER_OPTION_KEY, 999 ) );
+
+		// Values outside the range are always rejected, so nothing is saved.
+		$this->assertFalse( $reviews->set_reviews_per_page_option( false, Reviews::PER_PAGE_USER_OPTION_KEY, 0 ) );
+		$this->assertFalse( $reviews->set_reviews_per_page_option( false, Reviews::PER_PAGE_USER_OPTION_KEY, -5 ) );
+		$this->assertFalse( $reviews->set_reviews_per_page_option( 1000, Reviews::PER_PAGE_USER_OPTION_KEY, 1000 ) );
+
+		// Leaves other options untouched (returns the incoming status).
+		$this->assertFalse( $reviews->set_reviews_per_page_option( false, 'some_other_per_page', 55 ) );
+	}
+
+	/**
+	 * @testdox The dynamic screen-option filter saves valid values and rejects invalid values.
+	 *
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::__construct()
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::set_reviews_per_page_option()
+	 *
+	 * @return void
+	 */
+	public function test_set_reviews_per_page_option_filter(): void {
+		wc_get_container()->get( Reviews::class );
+		$hook = 'set_screen_option_' . Reviews::PER_PAGE_USER_OPTION_KEY;
+
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks -- Exercises WordPress's dynamic Screen Options filter.
+		$this->assertSame( 55, apply_filters( $hook, false, Reviews::PER_PAGE_USER_OPTION_KEY, 55 ) );
+
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks -- Exercises WordPress's dynamic Screen Options filter.
+		$this->assertFalse( apply_filters( $hook, 1000, Reviews::PER_PAGE_USER_OPTION_KEY, 1000 ) );
+	}
+
+	/**
+	 * @testdox The legacy filter is bridged and the dedicated reviews filter wins when it registers first.
+	 *
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::__construct()
+	 * @covers \Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews::apply_legacy_reviews_per_page_filter()
+	 *
+	 * @return void
+	 */
+	public function test_reviews_per_page_filter_bridge_and_precedence(): void {
+		$legacy    = static function () {
+			return 10;
+		};
+		$dedicated = static function () {
+			return 5;
+		};
+
+		add_filter( 'edit_comments_per_page', $legacy );
+
+		$sut = wc_get_container()->get( Reviews::class );
+		$sut->__construct();
+
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks -- Exercises the public Product Reviews per-page filter.
+		$this->assertSame( 10, (int) apply_filters( Reviews::PER_PAGE_USER_OPTION_KEY, 20 ) );
+
+		// Reset the dedicated hook to reproduce an extension loading before WooCommerce.
+		remove_all_filters( Reviews::PER_PAGE_USER_OPTION_KEY );
+		add_filter( Reviews::PER_PAGE_USER_OPTION_KEY, $dedicated );
+		$sut->__construct();
+
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks -- Exercises Product Reviews per-page filter precedence.
+		$this->assertSame( 5, (int) apply_filters( Reviews::PER_PAGE_USER_OPTION_KEY, 20 ) );
+	}
+
 	/**
 	 * @testdox `get_pending_count_bubble` will return the HTML for the pending reviews (awaiting moderation).
 	 *