From a2cb960ee3d04e4af5faafe3c0084aae5dd98f48 Mon Sep 17 00:00:00 2001 From: Muneeb Ashraf <58017595+muneeb-ashraf@users.noreply.github.com> Date: Thu, 3 Sep 2026 18:04:45 +0500 Subject: [PATCH 1/3] Filter media library by optimization status --- includes/admin/class-media-library.php | 162 +++++++++++++++++++++++ phpunit/tests/MediaLibraryTest.php | 175 +++++++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 phpunit/tests/MediaLibraryTest.php diff --git a/includes/admin/class-media-library.php b/includes/admin/class-media-library.php index 901c67f..3f0b706 100644 --- a/includes/admin/class-media-library.php +++ b/includes/admin/class-media-library.php @@ -9,6 +9,7 @@ use WebberZone\Image_Optimizer\Attachment_Meta; use WebberZone\Image_Optimizer\Converter; +use WebberZone\Image_Optimizer\Database; use WebberZone\Image_Optimizer\Processor; use WebberZone\Image_Optimizer\Queue; use WebberZone\Image_Optimizer\Util\Helpers; @@ -25,6 +26,21 @@ */ class Media_Library { + /** + * Query-string key for the optimization status filter. + * + * @since 1.3.0 + * @var string + */ + private const STATUS_FILTER = 'wzio_optimization_status'; + + /** + * Internal query flag for the failed-attachment SQL clause. + * + * @since 1.3.0 + * @var string + */ + private const FAILED_QUERY_VAR = 'wzio_filter_failed'; /** * Constructor. @@ -37,6 +53,9 @@ public function __construct() { Hook_Registry::add_filter( 'media_row_actions', array( $this, 'add_row_actions' ), 10, 2 ); Hook_Registry::add_filter( 'bulk_actions-upload', array( $this, 'add_bulk_actions' ) ); Hook_Registry::add_filter( 'handle_bulk_actions-upload', array( $this, 'handle_bulk_restore' ), 10, 3 ); + Hook_Registry::add_action( 'restrict_manage_posts', array( $this, 'render_status_filter' ), 10, 2 ); + Hook_Registry::add_action( 'pre_get_posts', array( $this, 'filter_by_status' ) ); + Hook_Registry::add_filter( 'posts_where', array( $this, 'filter_failed_attachments' ), 10, 2 ); Hook_Registry::add_action( 'admin_post_wzio_optimize_attachment', array( $this, 'handle_optimize' ) ); Hook_Registry::add_action( 'admin_post_wzio_restore_attachment', array( $this, 'handle_restore' ) ); Hook_Registry::add_action( 'wp_ajax_wzio_optimize_attachment', array( $this, 'ajax_optimize' ) ); @@ -152,6 +171,149 @@ public function add_column( $columns ) { return $columns; } + /** + * Add the optimization status filter to the media list table. + * + * @since 1.3.0 + * + * @param string $post_type Current post type. + * @param string $which Location of the filter controls. + * @return void + */ + public function render_status_filter( $post_type, $which ): void { + if ( 'attachment' !== $post_type || 'bar' !== $which || ! self::can_view_status_filter() ) { + return; + } + + $selected = self::get_requested_status(); + $options = array( + '' => __( 'All optimization statuses', 'webberzone-image-optimizer' ), + 'optimized' => __( 'Optimized', 'webberzone-image-optimizer' ), + 'unoptimized' => __( 'Not yet optimized', 'webberzone-image-optimizer' ), + 'failed' => __( 'Failed', 'webberzone-image-optimizer' ), + ); + ?> + + + is_main_query() + || 'attachment' !== $query->get( 'post_type' ) + || ! self::can_view_status_filter() + ) { + return; + } + + $status = self::get_requested_status(); + + if ( 'failed' === $status ) { + $query->set( self::FAILED_QUERY_VAR, true ); + return; + } + + if ( 'optimized' !== $status && 'unoptimized' !== $status ) { + return; + } + + $meta_clause = array( + 'key' => Attachment_Meta::META_KEY, + 'compare' => 'optimized' === $status ? 'EXISTS' : 'NOT EXISTS', + ); + $meta_query = $query->get( 'meta_query' ); + + if ( ! is_array( $meta_query ) || empty( $meta_query ) ) { + $query->set( 'meta_query', array( $meta_clause ) ); + return; + } + + $query->set( + 'meta_query', + array( + 'relation' => 'AND', + $meta_query, + $meta_clause, + ) + ); + } + + /** + * Limit a flagged media query to attachments in the failed queue state. + * + * @since 1.3.0 + * + * @param string $where Current WHERE clause. + * @param \WP_Query $query Current query. + * @return string Filtered WHERE clause. + */ + public function filter_failed_attachments( $where, $query ) { + global $wpdb; + + if ( true !== $query->get( self::FAILED_QUERY_VAR ) ) { + return $where; + } + + return $where . $wpdb->prepare( + ' AND %i.ID IN (SELECT attachment_id FROM %i WHERE status = %s)', + $wpdb->posts, + Database::get_table(), + Queue::FAILED + ); + } + + /** + * Whether the current user can use the status filter on this screen. + * + * @since 1.3.0 + * + * @return bool True when the optimization column is visible. + */ + private static function can_view_status_filter(): bool { + $screen = get_current_screen(); + + return current_user_can( 'upload_files' ) + && $screen + && 'upload' === $screen->id + && ! in_array( 'wzio', get_hidden_columns( $screen ), true ); + } + + /** + * Get a valid requested optimization status. + * + * @since 1.3.0 + * + * @return string Status, or an empty string when none is selected. + */ + private static function get_requested_status(): string { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $status = isset( $_GET[ self::STATUS_FILTER ] ) && is_string( $_GET[ self::STATUS_FILTER ] ) + ? sanitize_key( wp_unslash( $_GET[ self::STATUS_FILTER ] ) ) + : ''; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + return in_array( $status, array( 'optimized', 'unoptimized', 'failed' ), true ) ? $status : ''; + } + /** * Render the status column. * diff --git a/phpunit/tests/MediaLibraryTest.php b/phpunit/tests/MediaLibraryTest.php new file mode 100644 index 0000000..ecc6fca --- /dev/null +++ b/phpunit/tests/MediaLibraryTest.php @@ -0,0 +1,175 @@ +media_library = $reflection->newInstanceWithoutConstructor(); + + $this->original_pagenow = $GLOBALS['pagenow'] ?? null; + $this->original_main_query = $GLOBALS['wp_the_query'] ?? null; + + $GLOBALS['pagenow'] = 'upload.php'; + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + set_current_screen( 'upload' ); + } + + /** + * Restore globals changed by a test. + */ + public function tear_down() { + unset( $_GET['wzio_optimization_status'] ); + delete_user_option( get_current_user_id(), 'manageuploadcolumnshidden' ); + + if ( null === $this->original_pagenow ) { + unset( $GLOBALS['pagenow'] ); + } else { + $GLOBALS['pagenow'] = $this->original_pagenow; + } + + $GLOBALS['wp_the_query'] = $this->original_main_query; + + set_current_screen( 'front' ); + wp_set_current_user( 0 ); + + parent::tear_down(); + } + + /** + * The list view renders every status and preserves the selection. + */ + public function test_status_dropdown_renders_on_the_media_list_view() { + $_GET['wzio_optimization_status'] = 'failed'; + + ob_start(); + $this->media_library->render_status_filter( 'attachment', 'bar' ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'name="wzio_optimization_status"', $output ); + $this->assertStringContainsString( 'All optimization statuses', $output ); + $this->assertStringContainsString( 'Not yet optimized', $output ); + $this->assertStringContainsString( 'value="failed" selected', $output ); + } + + /** + * The dropdown stays hidden when its status column is hidden. + */ + public function test_status_dropdown_requires_a_visible_status_column() { + update_user_option( get_current_user_id(), 'manageuploadcolumnshidden', array( 'wzio' ) ); + + ob_start(); + $this->media_library->render_status_filter( 'attachment', 'bar' ); + $output = ob_get_clean(); + + $this->assertSame( '', $output ); + } + + /** + * Optimized filtering is added without replacing another meta query. + */ + public function test_optimized_filter_composes_with_existing_query_filters() { + $existing_meta_query = array( + array( + 'key' => '_example_key', + 'value' => 'example-value', + ), + ); + $query = $this->get_filtered_query( 'optimized', $existing_meta_query ); + $meta_query = $query->get( 'meta_query' ); + + $this->assertSame( 'AND', $meta_query['relation'] ); + $this->assertSame( $existing_meta_query, $meta_query[0] ); + $this->assertSame( Attachment_Meta::META_KEY, $meta_query[1]['key'] ); + $this->assertSame( 'EXISTS', $meta_query[1]['compare'] ); + $this->assertSame( 'image/jpeg', $query->get( 'post_mime_type' ) ); + $this->assertSame( 202601, $query->get( 'm' ) ); + } + + /** + * Unoptimized filtering selects attachments without conversion metadata. + */ + public function test_unoptimized_filter_uses_a_not_exists_meta_query() { + $query = $this->get_filtered_query( 'unoptimized' ); + $meta_query = $query->get( 'meta_query' ); + + $this->assertSame( Attachment_Meta::META_KEY, $meta_query[0]['key'] ); + $this->assertSame( 'NOT EXISTS', $meta_query[0]['compare'] ); + } + + /** + * Failed filtering adds a queue-table subquery to the existing WHERE clause. + */ + public function test_failed_filter_uses_the_queue_table() { + $query = $this->get_filtered_query( 'failed' ); + $where = $this->media_library->filter_failed_attachments( ' WHERE 1=1', $query ); + + $this->assertStringStartsWith( ' WHERE 1=1 AND ', $where ); + $this->assertStringContainsString( Database::get_table(), $where ); + $this->assertStringContainsString( 'attachment_id', $where ); + $this->assertStringContainsString( "status = 'failed'", $where ); + } + + /** + * Build a main media query and apply a requested status. + * + * @param string $status Requested status. + * @param array $meta_query Existing meta query. + * @return WP_Query Filtered query. + */ + private function get_filtered_query( $status, $meta_query = array() ) { + $_GET['wzio_optimization_status'] = $status; + + $query = new WP_Query(); + $query->set( 'post_type', 'attachment' ); + $query->set( 'post_mime_type', 'image/jpeg' ); + $query->set( 'm', 202601 ); + + if ( ! empty( $meta_query ) ) { + $query->set( 'meta_query', $meta_query ); + } + + $GLOBALS['wp_the_query'] = $query; + + $this->media_library->filter_by_status( $query ); + + return $query; + } +} From 8a9215311d6afa173e082ea0ebbdf1178c5fe1df Mon Sep 17 00:00:00 2001 From: Muneeb Ashraf <58017595+muneeb-ashraf@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:59:12 +0500 Subject: [PATCH 2/3] Address media status filter review --- includes/admin/class-media-library.php | 90 ++++++------- phpunit/tests/MediaLibraryTest.php | 170 ++++++++++++++++++------- readme.txt | 5 + 3 files changed, 179 insertions(+), 86 deletions(-) diff --git a/includes/admin/class-media-library.php b/includes/admin/class-media-library.php index 3f0b706..f10b27d 100644 --- a/includes/admin/class-media-library.php +++ b/includes/admin/class-media-library.php @@ -29,18 +29,18 @@ class Media_Library { /** * Query-string key for the optimization status filter. * - * @since 1.3.0 + * @since 1.1.0 * @var string */ private const STATUS_FILTER = 'wzio_optimization_status'; /** - * Internal query flag for the failed-attachment SQL clause. + * Internal query variable for the requested optimization status. * - * @since 1.3.0 + * @since 1.1.0 * @var string */ - private const FAILED_QUERY_VAR = 'wzio_filter_failed'; + private const STATUS_QUERY_VAR = 'wzio_filter_status'; /** * Constructor. @@ -55,7 +55,7 @@ public function __construct() { Hook_Registry::add_filter( 'handle_bulk_actions-upload', array( $this, 'handle_bulk_restore' ), 10, 3 ); Hook_Registry::add_action( 'restrict_manage_posts', array( $this, 'render_status_filter' ), 10, 2 ); Hook_Registry::add_action( 'pre_get_posts', array( $this, 'filter_by_status' ) ); - Hook_Registry::add_filter( 'posts_where', array( $this, 'filter_failed_attachments' ), 10, 2 ); + Hook_Registry::add_filter( 'posts_where', array( $this, 'filter_attachments_by_status' ), 10, 2 ); Hook_Registry::add_action( 'admin_post_wzio_optimize_attachment', array( $this, 'handle_optimize' ) ); Hook_Registry::add_action( 'admin_post_wzio_restore_attachment', array( $this, 'handle_restore' ) ); Hook_Registry::add_action( 'wp_ajax_wzio_optimize_attachment', array( $this, 'ajax_optimize' ) ); @@ -174,7 +174,7 @@ public function add_column( $columns ) { /** * Add the optimization status filter to the media list table. * - * @since 1.3.0 + * @since 1.1.0 * * @param string $post_type Current post type. * @param string $which Location of the filter controls. @@ -190,6 +190,7 @@ public function render_status_filter( $post_type, $which ): void { '' => __( 'All optimization statuses', 'webberzone-image-optimizer' ), 'optimized' => __( 'Optimized', 'webberzone-image-optimizer' ), 'unoptimized' => __( 'Not yet optimized', 'webberzone-image-optimizer' ), + 'skipped' => __( 'Skipped', 'webberzone-image-optimizer' ), 'failed' => __( 'Failed', 'webberzone-image-optimizer' ), ); ?> @@ -207,7 +208,7 @@ public function render_status_filter( $post_type, $which ): void { /** * Filter the media library query by optimization status. * - * @since 1.3.0 + * @since 1.1.0 * * @param \WP_Query $query Current query. * @return void @@ -220,71 +221,74 @@ public function filter_by_status( $query ): void { || 'upload.php' !== $pagenow || ! $query->is_main_query() || 'attachment' !== $query->get( 'post_type' ) - || ! self::can_view_status_filter() + || ! current_user_can( 'upload_files' ) ) { return; } $status = self::get_requested_status(); - if ( 'failed' === $status ) { - $query->set( self::FAILED_QUERY_VAR, true ); - return; - } - - if ( 'optimized' !== $status && 'unoptimized' !== $status ) { - return; + if ( '' !== $status ) { + $query->set( self::STATUS_QUERY_VAR, $status ); } - - $meta_clause = array( - 'key' => Attachment_Meta::META_KEY, - 'compare' => 'optimized' === $status ? 'EXISTS' : 'NOT EXISTS', - ); - $meta_query = $query->get( 'meta_query' ); - - if ( ! is_array( $meta_query ) || empty( $meta_query ) ) { - $query->set( 'meta_query', array( $meta_clause ) ); - return; - } - - $query->set( - 'meta_query', - array( - 'relation' => 'AND', - $meta_query, - $meta_clause, - ) - ); } /** - * Limit a flagged media query to attachments in the failed queue state. + * Limit a flagged media query to eligible attachments in the requested queue state. * - * @since 1.3.0 + * @since 1.1.0 * * @param string $where Current WHERE clause. * @param \WP_Query $query Current query. * @return string Filtered WHERE clause. */ - public function filter_failed_attachments( $where, $query ) { + public function filter_attachments_by_status( $where, $query ) { global $wpdb; - if ( true !== $query->get( self::FAILED_QUERY_VAR ) ) { + $status = $query->get( self::STATUS_QUERY_VAR ); + + if ( ! is_string( $status ) || ! in_array( $status, array( 'optimized', 'unoptimized', 'skipped', 'failed' ), true ) ) { return $where; } + $mimes = Helpers::SOURCE_MIME_TYPES; + $mime_placeholders = implode( ',', array_fill( 0, count( $mimes ), '%s' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + $where .= $wpdb->prepare( + " AND %i.post_mime_type IN ({$mime_placeholders})", + array_merge( array( $wpdb->posts ), $mimes ) + ); + + if ( 'unoptimized' === $status ) { + $terminal_statuses = array( Queue::DONE, Queue::SKIPPED, Queue::FAILED ); + $placeholders = implode( ',', array_fill( 0, count( $terminal_statuses ), '%s' ) ); + + return $where . $wpdb->prepare( + " AND %i.ID NOT IN (SELECT attachment_id FROM %i WHERE status IN ({$placeholders}))", + array_merge( array( $wpdb->posts, Database::get_table() ), $terminal_statuses ) + ); + } + + $queue_status = array( + 'optimized' => Queue::DONE, + 'skipped' => Queue::SKIPPED, + 'failed' => Queue::FAILED, + )[ $status ]; + return $where . $wpdb->prepare( ' AND %i.ID IN (SELECT attachment_id FROM %i WHERE status = %s)', $wpdb->posts, Database::get_table(), - Queue::FAILED + $queue_status ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } /** * Whether the current user can use the status filter on this screen. * - * @since 1.3.0 + * @since 1.1.0 * * @return bool True when the optimization column is visible. */ @@ -300,7 +304,7 @@ private static function can_view_status_filter(): bool { /** * Get a valid requested optimization status. * - * @since 1.3.0 + * @since 1.1.0 * * @return string Status, or an empty string when none is selected. */ @@ -311,7 +315,7 @@ private static function get_requested_status(): string { : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended - return in_array( $status, array( 'optimized', 'unoptimized', 'failed' ), true ) ? $status : ''; + return in_array( $status, array( 'optimized', 'unoptimized', 'skipped', 'failed' ), true ) ? $status : ''; } /** diff --git a/phpunit/tests/MediaLibraryTest.php b/phpunit/tests/MediaLibraryTest.php index ecc6fca..bb5508c 100644 --- a/phpunit/tests/MediaLibraryTest.php +++ b/phpunit/tests/MediaLibraryTest.php @@ -6,8 +6,8 @@ */ use WebberZone\Image_Optimizer\Admin\Media_Library; -use WebberZone\Image_Optimizer\Attachment_Meta; use WebberZone\Image_Optimizer\Database; +use WebberZone\Image_Optimizer\Queue; /** * Media library filtering controls and query clauses. @@ -36,11 +36,14 @@ class MediaLibraryTest extends WP_UnitTestCase { private $original_main_query; /** - * Prepare an upload screen and a media library instance without hooks. + * Prepare an upload screen, queue table and media library instance without hooks. */ public function set_up() { parent::set_up(); + Database::install(); + Queue::clear(); + $reflection = new ReflectionClass( Media_Library::class ); $this->media_library = $reflection->newInstanceWithoutConstructor(); @@ -58,6 +61,7 @@ public function set_up() { public function tear_down() { unset( $_GET['wzio_optimization_status'] ); delete_user_option( get_current_user_id(), 'manageuploadcolumnshidden' ); + Queue::clear(); if ( null === $this->original_pagenow ) { unset( $GLOBALS['pagenow'] ); @@ -86,6 +90,7 @@ public function test_status_dropdown_renders_on_the_media_list_view() { $this->assertStringContainsString( 'name="wzio_optimization_status"', $output ); $this->assertStringContainsString( 'All optimization statuses', $output ); $this->assertStringContainsString( 'Not yet optimized', $output ); + $this->assertStringContainsString( 'value="skipped"', $output ); $this->assertStringContainsString( 'value="failed" selected', $output ); } @@ -103,73 +108,152 @@ public function test_status_dropdown_requires_a_visible_status_column() { } /** - * Optimized filtering is added without replacing another meta query. + * Every status returns the matching eligible attachments and preserves other filters. */ - public function test_optimized_filter_composes_with_existing_query_filters() { - $existing_meta_query = array( - array( - 'key' => '_example_key', - 'value' => 'example-value', - ), + public function test_status_filters_return_exact_attachment_ids() { + $attachments = $this->seed_attachments(); + + $this->assertSame( + array( $attachments['optimized'] ), + $this->get_filtered_attachment_ids( 'optimized' ) + ); + $this->assertSame( + array( $attachments['unoptimized'], $attachments['pending'], $attachments['processing'] ), + $this->get_filtered_attachment_ids( 'unoptimized' ) + ); + $this->assertSame( + array( $attachments['skipped'] ), + $this->get_filtered_attachment_ids( 'skipped' ) + ); + $this->assertSame( + array( $attachments['failed'] ), + $this->get_filtered_attachment_ids( 'failed' ) ); - $query = $this->get_filtered_query( 'optimized', $existing_meta_query ); - $meta_query = $query->get( 'meta_query' ); - - $this->assertSame( 'AND', $meta_query['relation'] ); - $this->assertSame( $existing_meta_query, $meta_query[0] ); - $this->assertSame( Attachment_Meta::META_KEY, $meta_query[1]['key'] ); - $this->assertSame( 'EXISTS', $meta_query[1]['compare'] ); - $this->assertSame( 'image/jpeg', $query->get( 'post_mime_type' ) ); - $this->assertSame( 202601, $query->get( 'm' ) ); } /** - * Unoptimized filtering selects attachments without conversion metadata. + * Hiding the status column does not disable an active URL filter. */ - public function test_unoptimized_filter_uses_a_not_exists_meta_query() { - $query = $this->get_filtered_query( 'unoptimized' ); - $meta_query = $query->get( 'meta_query' ); + public function test_hidden_status_column_does_not_disable_filtering() { + $attachments = $this->seed_attachments(); + + update_user_option( get_current_user_id(), 'manageuploadcolumnshidden', array( 'wzio' ) ); - $this->assertSame( Attachment_Meta::META_KEY, $meta_query[0]['key'] ); - $this->assertSame( 'NOT EXISTS', $meta_query[0]['compare'] ); + $this->assertSame( + array( $attachments['failed'] ), + $this->get_filtered_attachment_ids( 'failed' ) + ); } /** - * Failed filtering adds a queue-table subquery to the existing WHERE clause. + * Seed eligible and unsupported attachments across every queue state. + * + * @return array Attachment IDs keyed by scenario. */ - public function test_failed_filter_uses_the_queue_table() { - $query = $this->get_filtered_query( 'failed' ); - $where = $this->media_library->filter_failed_attachments( ' WHERE 1=1', $query ); - - $this->assertStringStartsWith( ' WHERE 1=1 AND ', $where ); - $this->assertStringContainsString( Database::get_table(), $where ); - $this->assertStringContainsString( 'attachment_id', $where ); - $this->assertStringContainsString( "status = 'failed'", $where ); + private function seed_attachments(): array { + $attachments = array( + 'optimized' => $this->create_attachment( 'optimized.jpg', 'image/jpeg' ), + 'unoptimized' => $this->create_attachment( 'unoptimized.jpg', 'image/jpeg' ), + 'skipped' => $this->create_attachment( 'skipped.jpg', 'image/jpeg' ), + 'failed' => $this->create_attachment( 'failed.jpg', 'image/jpeg' ), + 'pending' => $this->create_attachment( 'pending.jpg', 'image/jpeg' ), + 'processing' => $this->create_attachment( 'processing.jpg', 'image/jpeg' ), + 'unsupported' => $this->create_attachment( 'unsupported.pdf', 'application/pdf' ), + 'optimized_png' => $this->create_attachment( 'optimized.png', 'image/png' ), + 'optimized_next_month' => $this->create_attachment( 'optimized-next-month.jpg', 'image/jpeg', '2026-02-15 12:00:00' ), + ); + + $this->set_queue_status( $attachments['optimized'], Queue::DONE ); + $this->set_queue_status( $attachments['skipped'], Queue::SKIPPED ); + $this->set_queue_status( $attachments['failed'], Queue::FAILED ); + $this->set_queue_status( $attachments['pending'], Queue::PENDING ); + $this->set_queue_status( $attachments['processing'], Queue::PROCESSING ); + $this->set_queue_status( $attachments['optimized_png'], Queue::DONE ); + $this->set_queue_status( $attachments['optimized_next_month'], Queue::DONE ); + + return $attachments; } /** - * Build a main media query and apply a requested status. + * Create an attachment fixture. * - * @param string $status Requested status. - * @param array $meta_query Existing meta query. - * @return WP_Query Filtered query. + * @param string $file Fixture filename. + * @param string $mime_type Attachment MIME type. + * @param string $date Attachment date. + * @return int Attachment ID. */ - private function get_filtered_query( $status, $meta_query = array() ) { + private function create_attachment( string $file, string $mime_type, string $date = '2026-01-15 12:00:00' ): int { + return (int) self::factory()->attachment->create( + array( + 'file' => $file, + 'post_title' => $file, + 'post_mime_type' => $mime_type, + 'post_status' => 'inherit', + 'post_date' => $date, + 'post_date_gmt' => $date, + ) + ); + } + + /** + * Put an attachment into a queue state using the public queue API. + * + * @param int $attachment_id Attachment ID. + * @param string $status Queue status. + * @return void + */ + private function set_queue_status( int $attachment_id, string $status ): void { + Queue::add( array( $attachment_id ), true ); + + if ( Queue::PROCESSING === $status ) { + Queue::claim_attachment( $attachment_id ); + } elseif ( Queue::PENDING !== $status ) { + $row_id = Queue::get_id( $attachment_id ); + + if ( Queue::FAILED === $status ) { + for ( $attempt = 0; $attempt < Queue::MAX_ATTEMPTS; ++$attempt ) { + Queue::complete( $row_id, Queue::FAILED ); + } + } else { + Queue::complete( $row_id, $status ); + } + } + + $this->assertSame( $status, Queue::get_status( $attachment_id ) ); + } + + /** + * Run a main media query with the requested status and existing MIME/date filters. + * + * @param string $status Requested status. + * @return array Matching attachment IDs. + */ + private function get_filtered_attachment_ids( string $status ): array { $_GET['wzio_optimization_status'] = $status; $query = new WP_Query(); $query->set( 'post_type', 'attachment' ); + $query->set( 'post_status', 'inherit' ); $query->set( 'post_mime_type', 'image/jpeg' ); $query->set( 'm', 202601 ); - - if ( ! empty( $meta_query ) ) { - $query->set( 'meta_query', $meta_query ); - } + $query->set( 'fields', 'ids' ); + $query->set( 'orderby', 'ID' ); + $query->set( 'order', 'ASC' ); + $query->set( 'posts_per_page', -1 ); + $query->set( 'no_found_rows', true ); + $query->set( 'suppress_filters', false ); $GLOBALS['wp_the_query'] = $query; $this->media_library->filter_by_status( $query ); + add_filter( 'posts_where', array( $this->media_library, 'filter_attachments_by_status' ), 10, 2 ); + + try { + $attachment_ids = $query->get_posts(); + } finally { + remove_filter( 'posts_where', array( $this->media_library, 'filter_attachments_by_status' ), 10 ); + } - return $query; + return array_map( 'intval', $attachment_ids ); } } diff --git a/readme.txt b/readme.txt index dd21ef0..44a359d 100644 --- a/readme.txt +++ b/readme.txt @@ -108,6 +108,11 @@ Yes. Because the format choice happens in the browser rather than on the server, == Changelog == += 1.1.0 = + +* Features: + * Filter Media Library images by optimized, not-yet-optimized, skipped and failed status. + = 1.0.2 = Release date: 27 August 2026 From c571f51548cf588bd562c8203209e338cc950cfa Mon Sep 17 00:00:00 2001 From: Muneeb Ashraf <58017595+muneeb-ashraf@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:43:47 +0500 Subject: [PATCH 3/3] Fix media library test setup --- phpunit/tests/MediaLibraryTest.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/phpunit/tests/MediaLibraryTest.php b/phpunit/tests/MediaLibraryTest.php index bb5508c..d6432d2 100644 --- a/phpunit/tests/MediaLibraryTest.php +++ b/phpunit/tests/MediaLibraryTest.php @@ -14,6 +14,24 @@ */ class MediaLibraryTest extends WP_UnitTestCase { + /** + * Install the queue table outside the per-test database transaction. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + Database::install(); + } + + /** + * Remove the queue table after all tests in this class have run. + */ + public static function tear_down_after_class() { + Database::drop_table(); + + parent::tear_down_after_class(); + } + /** * Media library integration under test. * @@ -41,7 +59,6 @@ class MediaLibraryTest extends WP_UnitTestCase { public function set_up() { parent::set_up(); - Database::install(); Queue::clear(); $reflection = new ReflectionClass( Media_Library::class ); @@ -91,7 +108,7 @@ public function test_status_dropdown_renders_on_the_media_list_view() { $this->assertStringContainsString( 'All optimization statuses', $output ); $this->assertStringContainsString( 'Not yet optimized', $output ); $this->assertStringContainsString( 'value="skipped"', $output ); - $this->assertStringContainsString( 'value="failed" selected', $output ); + $this->assertStringContainsString( 'value="failed" selected=\'selected\'', $output ); } /**