From 1d0507cdc8e1047cd578b4980270d95a9ecdb600 Mon Sep 17 00:00:00 2001 From: Kamran Abdul Aziz Date: Wed, 9 Sep 2026 22:27:33 +0530 Subject: [PATCH 1/2] Translation Events: Show a notice after trashing, restoring, or deleting an event Trashing, restoring, or permanently deleting an event redirects the user away from the page the action was triggered from, and nothing was shown afterwards, so there was no confirmation that the action actually happened. This sets a GlotPress notice before each redirect, so it is carried across in the notice cookie and rendered on the destination page, in all three places an event can be trashed, restored, or deleted: the Delete Event button on the edit form, the trash and restore links handled by the trash route, and the permanent delete link handled by the delete route. The failure path of the form handler surfaces an error notice the same way. Adds tests covering all five paths by capturing the notice cookie through the gp_set_cookie filter. This ports the archived standalone repository's pull request 392 to this repository. --- .../includes/event/event-form-handler.php | 21 +- .../includes/routes/event/delete.php | 1 + .../includes/routes/event/trash.php | 2 + .../tests/routes/Event_Notices_Test.php | 246 ++++++++++++++++++ 4 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event/event-form-handler.php b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event/event-form-handler.php index f50da3787e..1c192df71a 100644 --- a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event/event-form-handler.php +++ b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event/event-form-handler.php @@ -88,14 +88,25 @@ public function handle( array $form_data ): void { wp_send_json_error( esc_html__( 'Event has stats so it cannot be deleted.', 'gp-translation-events' ), 422 ); } - if ( false === $this->event_repository->trash_event( $event ) ) { - $response_message = esc_html__( 'Failed to delete event.', 'gp-translation-events' ); - $event_status = $event->status(); - } else { - $response_message = esc_html__( 'Event deleted successfully.', 'gp-translation-events' ); + $trashed = false !== $this->event_repository->trash_event( $event ); + if ( $trashed ) { + $response_message = esc_html__( 'Event moved to the trash.', 'gp-translation-events' ); $event_status = 'trashed'; $this->notifications_schedule->delete_scheduled_emails( $event_id ); + } else { + $response_message = esc_html__( 'Failed to delete event.', 'gp-translation-events' ); + $event_status = $event->status(); } + + // The client redirects to My Events regardless of the outcome, so + // surface the result there as a GlotPress notice instead of losing + // it in the redirect. + gp_notice_set( + $trashed + ? __( 'Event moved to the trash.', 'gp-translation-events' ) + : __( 'Failed to delete event.', 'gp-translation-events' ), + $trashed ? 'notice' : 'error' + ); } else { // Create or update event. diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/delete.php b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/delete.php index 4f8e5c43f8..4017f98c28 100644 --- a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/delete.php +++ b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/delete.php @@ -55,6 +55,7 @@ public function handle( int $event_id ): void { $this->event_repository->delete_event( $event ); + gp_notice_set( __( 'Event permanently deleted.', 'gp-translation-events' ) ); wp_safe_redirect( Urls::events_home() ); $this->exit_(); } diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/trash.php b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/trash.php index 681a654dfd..d5eadfc677 100644 --- a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/trash.php +++ b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/routes/event/trash.php @@ -53,11 +53,13 @@ public function handle( int $event_id ): void { if ( ! $event->is_trashed() ) { // Trash. $this->event_repository->trash_event( $event ); + gp_notice_set( __( 'Event moved to the trash.', 'gp-translation-events' ) ); wp_safe_redirect( Urls::events_home() ); } else { // Restore. $event->set_status( 'draft' ); $this->event_repository->update_event( $event ); + gp_notice_set( __( 'Event restored.', 'gp-translation-events' ) ); wp_safe_redirect( Urls::event_edit( $event->id() ) ); } diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php new file mode 100644 index 0000000000..4eebaf84c2 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php @@ -0,0 +1,246 @@ +` + * cookies. These tests capture those cookies through the `gp_set_cookie` + * filter, which also suppresses the real `setcookie()` call so it does not + * warn about headers already sent under PHPUnit. + */ +class Event_Notices_Test extends Base_Test { + /** + * The notice messages captured from the `gp_set_cookie` filter, keyed by + * the GlotPress notice key ('notice' or 'error'). + * + * @var array + */ + private array $notices = array(); + + /** + * @var callable + */ + private $cookie_capture; + + /** + * @var callable + */ + private $redirect_guard; + + /** + * @return void + */ + public function setUp(): void { + parent::setUp(); + + // Any logged-in user can manage events, so the capability checks pass + // and the redirect path under test is reached. + add_filter( 'gp_translation_events_can_crud_event', '__return_true' ); + + $this->cookie_capture = function ( $args ) { + // GlotPress stores notices in `_gp_notice_` cookies; capture + // the message under its key so both 'notice' and 'error' are seen. + if ( isset( $args[0], $args[1] ) && 0 === strpos( (string) $args[0], '_gp_notice_' ) ) { + $key = substr( (string) $args[0], strlen( '_gp_notice_' ) ); + $this->notices[ $key ] = $args[1]; + } + + // Returning false stops GlotPress from actually calling setcookie(). + return false; + }; + add_filter( 'gp_set_cookie', $this->cookie_capture ); + + // Stop the redirect from emitting a header under PHPUnit. + $this->redirect_guard = '__return_false'; + add_filter( 'wp_redirect', $this->redirect_guard ); + } + + /** + * @return void + */ + public function tearDown(): void { + remove_filter( 'wp_redirect', $this->redirect_guard ); + remove_filter( 'gp_set_cookie', $this->cookie_capture ); + remove_filter( 'gp_translation_events_can_crud_event', '__return_true' ); + unset( $_GET['_wpnonce'] ); + + parent::tearDown(); + } + + /** + * @return void + */ + public function test_trashing_an_event_sets_a_notice() { + $event_id = ( new Event_Factory() )->create_active( $this->now ); + + $this->run_route( Trash_Route::class, $event_id, 'trash_translation_event_' . $event_id ); + + $this->assertSame( 'Event moved to the trash.', $this->notices['notice'] ?? null ); + } + + /** + * @return void + */ + public function test_restoring_an_event_sets_a_notice() { + $factory = new Event_Factory(); + $event_id = $factory->create_active( $this->now ); + Translation_Events::get_event_repository()->trash_event( + Translation_Events::get_event_repository()->get_event( $event_id ) + ); + + // The trash route toggles: a trashed event is restored. + $this->run_route( Trash_Route::class, $event_id, 'trash_translation_event_' . $event_id ); + + $this->assertSame( 'Event restored.', $this->notices['notice'] ?? null ); + } + + /** + * @return void + */ + public function test_permanently_deleting_an_event_sets_a_notice() { + $event_id = ( new Event_Factory() )->create_active( $this->now ); + Translation_Events::get_event_repository()->trash_event( + Translation_Events::get_event_repository()->get_event( $event_id ) + ); + + $this->run_route( Delete_Route::class, $event_id, 'delete_translation_event_' . $event_id ); + + $this->assertSame( 'Event permanently deleted.', $this->notices['notice'] ?? null ); + } + + /** + * The "Delete event" button on the edit form trashes over AJAX and then + * redirects the browser to My Events, so the same notice must be set there. + * + * @return void + */ + public function test_trashing_via_the_form_handler_sets_a_notice() { + $event_id = ( new Event_Factory() )->create_active( $this->now ); + wp_set_current_user( $this->factory->user->create() ); + + $form_data = array( + 'form_name' => 'trash_event', + 'event_id' => (string) $event_id, + '_event_nonce' => wp_create_nonce( '_event_nonce' ), + ); + + // The handler ends in wp_send_json_success(), which calls wp_die(). + // Route that through the AJAX die handler and make it throw so the + // notice set just before it can be asserted. + add_filter( 'wp_doing_ajax', '__return_true' ); + $die_handler = function () { + return function () { + throw new Exception( 'wp_send_json' ); + }; + }; + add_filter( 'wp_die_ajax_handler', $die_handler ); + + $handler = new Event_Form_Handler( $this->now, Translation_Events::get_event_repository() ); + + ob_start(); + try { + $handler->handle( $form_data ); + } catch ( Exception $e ) { + // Expected: wp_send_json_success() reached wp_die(). + $this->assertSame( 'wp_send_json', $e->getMessage() ); + } finally { + ob_end_clean(); + remove_filter( 'wp_die_ajax_handler', $die_handler ); + remove_filter( 'wp_doing_ajax', '__return_true' ); + } + + $this->assertSame( 'Event moved to the trash.', $this->notices['notice'] ?? null ); + } + + /** + * When the trash fails, the user is still redirected to My Events, so the + * failure must be surfaced there as an error notice. + * + * @return void + */ + public function test_a_failed_trash_via_the_form_handler_sets_an_error_notice() { + $event_id = ( new Event_Factory() )->create_active( $this->now ); + wp_set_current_user( $this->factory->user->create() ); + + $form_data = array( + 'form_name' => 'trash_event', + 'event_id' => (string) $event_id, + '_event_nonce' => wp_create_nonce( '_event_nonce' ), + ); + + // Force the trash to fail so the error branch is exercised. + add_filter( 'pre_trash_post', '__return_false' ); + + add_filter( 'wp_doing_ajax', '__return_true' ); + $die_handler = function () { + return function () { + throw new Exception( 'wp_send_json' ); + }; + }; + add_filter( 'wp_die_ajax_handler', $die_handler ); + + $handler = new Event_Form_Handler( $this->now, Translation_Events::get_event_repository() ); + + ob_start(); + try { + $handler->handle( $form_data ); + } catch ( Exception $e ) { + $this->assertSame( 'wp_send_json', $e->getMessage() ); + } finally { + ob_end_clean(); + remove_filter( 'wp_die_ajax_handler', $die_handler ); + remove_filter( 'wp_doing_ajax', '__return_true' ); + remove_filter( 'pre_trash_post', '__return_false' ); + } + + $this->assertSame( 'Failed to delete event.', $this->notices['error'] ?? null ); + $this->assertArrayNotHasKey( 'notice', $this->notices ); + } + + /** + * Run a route to completion with a valid nonce as a logged-in user. + * + * @param string $route_class Route to exercise. + * @param int $event_id Event the route acts on. + * @param string $nonce_action Nonce action the route verifies. + * @return void + */ + private function run_route( string $route_class, int $event_id, string $nonce_action ) { + wp_set_current_user( $this->factory->user->create() ); + $_GET['_wpnonce'] = wp_create_nonce( $nonce_action ); + + $route = new $route_class(); + $route->fake_request = true; + + // The route renders a redirect template on success, which would + // otherwise garble PHPUnit's output. On success it also calls exit_(), + // which throws under a faked request to halt the way a real exit would. + ob_start(); + try { + $route->handle( $event_id ); + } catch ( GP_Route_Exit_Exception $e ) { + // Expected: the route reached its exit after redirecting. + $this->assertInstanceOf( GP_Route_Exit_Exception::class, $e ); + } finally { + ob_end_clean(); + } + } +} From 73b4cbc285f90f7b37e5aaf0e010d628d3819bff Mon Sep 17 00:00:00 2001 From: Kamran Abdul Aziz Date: Wed, 9 Sep 2026 22:49:42 +0530 Subject: [PATCH 2/2] Add short descriptions to the test doc comments The PHP Coding Standards check requires a short description in every doc comment. Adds one to the two filter callback properties, setUp(), tearDown(), and the three test methods that had only a tag. --- .../tests/routes/Event_Notices_Test.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php index 4eebaf84c2..80a055eceb 100644 --- a/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php +++ b/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/tests/routes/Event_Notices_Test.php @@ -36,16 +36,22 @@ class Event_Notices_Test extends Base_Test { private array $notices = array(); /** + * The `gp_set_cookie` filter callback that captures notice cookies. + * * @var callable */ private $cookie_capture; /** + * The `wp_redirect` filter callback that stops redirect headers under PHPUnit. + * * @var callable */ private $redirect_guard; /** + * Registers the cookie capture and redirect guard before each test. + * * @return void */ public function setUp(): void { @@ -74,6 +80,8 @@ public function setUp(): void { } /** + * Removes the filters registered in setUp(). + * * @return void */ public function tearDown(): void { @@ -86,6 +94,8 @@ public function tearDown(): void { } /** + * Trashing an event through the trash route sets a success notice. + * * @return void */ public function test_trashing_an_event_sets_a_notice() { @@ -97,6 +107,8 @@ public function test_trashing_an_event_sets_a_notice() { } /** + * Restoring a trashed event through the trash route sets a success notice. + * * @return void */ public function test_restoring_an_event_sets_a_notice() { @@ -113,6 +125,8 @@ public function test_restoring_an_event_sets_a_notice() { } /** + * Permanently deleting a trashed event sets a success notice. + * * @return void */ public function test_permanently_deleting_an_event_sets_a_notice() {