diff --git a/src/js/_enqueues/wp/heartbeat.js b/src/js/_enqueues/wp/heartbeat.js index 65635177d9f66..a286a9ecee826 100644 --- a/src/js/_enqueues/wp/heartbeat.js +++ b/src/js/_enqueues/wp/heartbeat.js @@ -100,7 +100,12 @@ // Timer that keeps track of how long needs to be waited before connecting to // the server again. - beatTimer: 0 + beatTimer: 0, + + // Post lock window in milliseconds. Synced from PHP via heartbeat_settings. + // Used to calculate the background tab heartbeat interval. + // Default matches the wp_check_post_lock_window default of 150 seconds. + postLockWindow: 150000 }; /** @@ -112,7 +117,7 @@ * @return {void} */ function initialize() { - var options, hidden, visibilityState, visibilitychange; + var options, hidden, visibilityState, visibilitychange, postLockWindow; if ( typeof window.pagenow === 'string' ) { settings.screenId = window.pagenow; @@ -172,6 +177,14 @@ if ( options.suspension === 'disable' ) { settings.suspendEnabled = false; } + + if ( options.post_lock_window ) { + postLockWindow = parseInt( options.post_lock_window, 10 ); + + if ( postLockWindow > 0 ) { + settings.postLockWindow = postLockWindow * 1000; + } + } } // Convert to milliseconds. @@ -509,7 +522,11 @@ } if ( ! settings.hasFocus ) { - interval = 120000; // 120 seconds. Post locks expire after 150 seconds. + // Fire 30 seconds before the post lock window expires so backgrounded + // tabs always refresh the lock in time. For the default 150s window this + // equals 120s, preserving existing behaviour. Floored at 5s for very + // short custom windows. + interval = Math.max( 5000, settings.postLockWindow - 30000 ); } else if ( settings.countdown > 0 && settings.tempInterval ) { interval = settings.tempInterval; settings.countdown--; diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index f021aedb8a5fb..cd8a076b0e5ab 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -1325,7 +1325,7 @@ function wp_refresh_heartbeat_nonces( $response ) { } /** - * Disables suspension of Heartbeat on the Add/Edit Post screens. + * Disables suspension of Heartbeat and sets post lock data on the Add/Edit Post screens. * * @since 3.8.0 * @@ -1339,6 +1339,9 @@ function wp_heartbeat_set_suspension( $settings ) { if ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) { $settings['suspension'] = 'disable'; + + /** This filter is documented in wp-admin/includes/ajax-actions.php */ + $settings['post_lock_window'] = apply_filters( 'wp_check_post_lock_window', 150 ); } return $settings; diff --git a/tests/phpunit/tests/admin/includes/misc/WpHeartbeatSetSuspension_Test.php b/tests/phpunit/tests/admin/includes/misc/WpHeartbeatSetSuspension_Test.php index e799efda53353..739991f8a8fe5 100644 --- a/tests/phpunit/tests/admin/includes/misc/WpHeartbeatSetSuspension_Test.php +++ b/tests/phpunit/tests/admin/includes/misc/WpHeartbeatSetSuspension_Test.php @@ -79,4 +79,89 @@ public function data_wp_heartbeat_set_suspension(): array { ), ); } + + /** + * Tests that wp_heartbeat_set_suspension() exposes the default post lock window on post screens. + * + * @dataProvider data_post_screens + * + * @ticket 65171 + * + * @param string $pagenow_value The value for the $pagenow global. + */ + public function test_wp_heartbeat_set_suspension_sets_post_lock_window_on_post_screens( $pagenow_value ) { + global $pagenow; + + $pagenow = $pagenow_value; + + $result = wp_heartbeat_set_suspension( array() ); + + $this->assertArrayHasKey( 'post_lock_window', $result, "'post_lock_window' should be present when \$pagenow is {$pagenow_value}." ); + $this->assertSame( 150, $result['post_lock_window'], "'post_lock_window' should default to 150 when \$pagenow is {$pagenow_value}." ); + } + + /** + * Tests that wp_heartbeat_set_suspension() does not expose a post lock window on non-post screens. + * + * @dataProvider data_non_post_screens + * + * @ticket 65171 + * + * @param string $pagenow_value The value for the $pagenow global. + */ + public function test_wp_heartbeat_set_suspension_does_not_set_post_lock_window_on_other_screens( $pagenow_value ) { + global $pagenow; + + $pagenow = $pagenow_value; + + $result = wp_heartbeat_set_suspension( array() ); + + $this->assertArrayNotHasKey( 'post_lock_window', $result, "'post_lock_window' should not be set when \$pagenow is {$pagenow_value}." ); + } + + /** + * Tests that wp_heartbeat_set_suspension() honors the wp_check_post_lock_window filter. + * + * @ticket 65171 + */ + public function test_wp_heartbeat_set_suspension_post_lock_window_respects_filter() { + global $pagenow; + + $pagenow = 'post.php'; + + add_filter( + 'wp_check_post_lock_window', + static function () { + return 60; + } + ); + + $result = wp_heartbeat_set_suspension( array() ); + + $this->assertSame( 60, $result['post_lock_window'], "'post_lock_window' should reflect the wp_check_post_lock_window filter value." ); + } + + /** + * Data provider: $pagenow values for the Add/Edit Post screens. + * + * @return array + */ + public function data_post_screens(): array { + return array( + 'post.php' => array( 'pagenow_value' => 'post.php' ), + 'post-new.php' => array( 'pagenow_value' => 'post-new.php' ), + ); + } + + /** + * Data provider: $pagenow values for screens that are not Add/Edit Post. + * + * @return array + */ + public function data_non_post_screens(): array { + return array( + 'index.php' => array( 'pagenow_value' => 'index.php' ), + 'edit.php' => array( 'pagenow_value' => 'edit.php' ), + ); + } }