diff --git a/src/wp-admin/includes/nav-menu.php b/src/wp-admin/includes/nav-menu.php index ea7de40a84152..4ae399f1b5084 100644 --- a/src/wp-admin/includes/nav-menu.php +++ b/src/wp-admin/includes/nav-menu.php @@ -488,30 +488,10 @@ function wp_nav_menu_item_post_type_meta_box( $data_object, $box ) { $suppress_page_ids[] = $privacy_policy_page->ID; } } - - // Add suppression array to arguments for WP_Query. - if ( ! empty( $suppress_page_ids ) ) { - $args['post__not_in'] = $suppress_page_ids; - } } // @todo Transient caching of these results with proper invalidation on updating of a post of this type. $get_posts = new WP_Query(); - $posts = $get_posts->query( $args ); - - // Only suppress and insert when more than just suppression pages available. - if ( ! $get_posts->post_count ) { - if ( ! empty( $suppress_page_ids ) ) { - unset( $args['post__not_in'] ); - $get_posts = new WP_Query(); - $posts = $get_posts->query( $args ); - } else { - echo '

' . __( 'No items.' ) . '

'; - return; - } - } elseif ( ! empty( $important_pages ) ) { - $posts = array_merge( $important_pages, $posts ); - } $num_pages = $get_posts->max_num_pages; @@ -740,6 +720,28 @@ class="categorychecklist form-no-clear" query( $args ); + + // Only suppress and insert when more than just suppression pages available. + if ( ! $get_posts->post_count ) { + if ( ! empty( $suppress_page_ids ) ) { + unset( $args['post__not_in'] ); + $get_posts = new WP_Query(); + $posts = $get_posts->query( $args ); + } else { + echo '

' . __( 'No items.' ) . '

'; + return; + } + } elseif ( ! empty( $important_pages ) ) { + $posts = array_merge( $important_pages, $posts ); + } + if ( $post_type->has_archive ) { $_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1; array_unshift( diff --git a/tests/phpunit/tests/admin/includesNavMenuMetaBox.php b/tests/phpunit/tests/admin/includesNavMenuMetaBox.php new file mode 100644 index 0000000000000..6f20a594428ab --- /dev/null +++ b/tests/phpunit/tests/admin/includesNavMenuMetaBox.php @@ -0,0 +1,271 @@ +post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Front Page', + 'post_status' => 'publish', + ) + ); + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $front_page ); + + $old_page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Old Page', + 'post_status' => 'publish', + 'post_date' => '2000-01-01 00:00:00', + ) + ); + + $recent_page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Recent Page', + 'post_status' => 'publish', + ) + ); + + // The right order should be + // 1. Front Page + // 2. Old Page + // 3. Recent Page + // First important pages, then strictly by title. + + $post_type = get_post_type_object( 'page' ); + $box = array( + 'args' => $post_type, + ); + + ob_start(); + wp_nav_menu_item_post_type_meta_box( null, $box ); + $output = ob_get_clean(); + + // Extract only the "View All" tab content to avoid interference from other tabs. + $dom = new DOMDocument(); + $dom->loadHTML( $output ); + $xpath = new DOMXPath( $dom ); + $page_all_div = $xpath->query( '//div[@id="page-all"]' )->item( 0 ); + $view_all_content = $page_all_div ? $dom->saveHTML( $page_all_div ) : ''; + + $this->assertNotEmpty( $view_all_content, 'Should find the View All tab content' ); + + $this->assertStringContainsString( 'Recent Page', $view_all_content, 'Recent Page should be present in View All tab' ); + $this->assertStringContainsString( 'Front Page', $view_all_content, 'Front Page should be present in View All tab' ); + + $front_page_position = strpos( $view_all_content, 'Front Page' ); + $recent_page_position = strpos( $view_all_content, 'Recent Page' ); + $old_page_position = strpos( $view_all_content, 'Old Page' ); + + $this->assertLessThan( $recent_page_position, $front_page_position, 'Front Page should appear before Recent Page due to important pages being merged first' ); + $this->assertLessThan( $recent_page_position, $old_page_position, 'Old Page should appear before Recent Page according to the order of titles ' ); + } + + /** + * Test that most-recent tab shows recent pages including important pages. + * + * @ticket 63473 + * + * @covers ::wp_nav_menu_item_post_type_meta_box + */ + public function test_most_recent_tab_displays_recent_pages_with_important_pages() { + $front_page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Front Page', + 'post_status' => 'publish', + 'post_date' => '2010-01-01 00:00:00', + ) + ); + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $front_page ); + + $old_page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Old Page', + 'post_status' => 'publish', + 'post_date' => '2000-01-01 00:00:00', + ) + ); + + $recent_page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Recent Page', + 'post_status' => 'publish', + 'post_date' => '2020-01-01 00:00:00', + ) + ); + + // The right order should be: + // 1. Recent Page + // 2. Front Page + // 3. Old Page + // Strictly by date. + + $post_type = get_post_type_object( 'page' ); + $box = array( + 'args' => $post_type, + ); + + ob_start(); + wp_nav_menu_item_post_type_meta_box( null, $box ); + $output = ob_get_clean(); + + // Extract only the "Most Recent" tab content to avoid interference from other tabs. + $dom = new DOMDocument(); + $dom->loadHTML( $output ); + $xpath = new DOMXPath( $dom ); + $page_all_div = $xpath->query( '//div[@id="tabs-panel-posttype-page-most-recent"]' )->item( 0 ); + $most_recent_content = $page_all_div ? $dom->saveHTML( $page_all_div ) : ''; + + $this->assertNotEmpty( $most_recent_content, 'Should find the Most Recent tab content' ); + + $this->assertStringContainsString( 'Recent Page', $most_recent_content, 'Recent Page should be present in Most Recent tab' ); + $this->assertStringContainsString( 'Front Page', $most_recent_content, 'Front Page should be present in Most Recent tab' ); + $this->assertStringContainsString( 'Old Page', $most_recent_content, 'Old Page should be present in Most Recent tab' ); + + $front_page_position = strpos( $most_recent_content, 'Front Page' ); + $recent_page_position = strpos( $most_recent_content, 'Recent Page' ); + $old_page_position = strpos( $most_recent_content, 'Old Page' ); + + $this->assertLessThan( $front_page_position, $recent_page_position, 'Recent Page should appear before Front Page because its the last created page' ); + $this->assertLessThan( $old_page_position, $front_page_position, 'Front Page should appear before Old Page because its more recent' ); + $this->assertLessThan( $old_page_position, $recent_page_position, 'Old Page should appear before Recent Page according to the order of titles ' ); + } + + /** + * Test that when only important pages exist, they are displayed properly. + * + * @covers ::wp_nav_menu_item_post_type_meta_box + */ + public function test_view_all_tab_handles_only_important_pages_scenario() { + $front_page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Front Page Only', + 'post_status' => 'publish', + ) + ); + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $front_page ); + + $post_type = get_post_type_object( 'page' ); + $box = array( + 'args' => $post_type, + ); + + ob_start(); + wp_nav_menu_item_post_type_meta_box( null, $box ); + $output = ob_get_clean(); + + // Extract only the "View All" tab content to avoid interference from other tabs. + $dom = new DOMDocument(); + $dom->loadHTML( $output ); + $xpath = new DOMXPath( $dom ); + $page_all_div = $xpath->query( '//div[@id="page-all"]' )->item( 0 ); + $view_all_content = $page_all_div ? $dom->saveHTML( $page_all_div ) : ''; + + $this->assertNotEmpty( $view_all_content, 'Should find the View All tab content' ); + + $this->assertStringContainsString( 'Front Page Only', $view_all_content, 'Front Page Only should be present in View All tab' ); + $this->assertStringNotContainsString( 'No items.', $view_all_content, 'No items message should not be shown' ); + } + + /** + * Test that when no pages exist at all, "No items" message is shown. + * + * @covers ::wp_nav_menu_item_post_type_meta_box + */ + public function test_view_all_tab_shows_no_items_when_no_pages_exist() { + + update_option( 'show_on_front', 'posts' ); + $post_type = get_post_type_object( 'page' ); + $box = array( + 'args' => $post_type, + ); + ob_start(); + wp_nav_menu_item_post_type_meta_box( null, $box ); + $output = ob_get_clean(); + + // Extract only the "View All" tab content to avoid interference from other tabs. + $dom = new DOMDocument(); + $dom->loadHTML( $output ); + $xpath = new DOMXPath( $dom ); + $page_all_div = $xpath->query( '//div[@id="page-all"]' )->item( 0 ); + $view_all_content = $page_all_div ? $dom->saveHTML( $page_all_div ) : ''; + + $this->assertNotEmpty( $view_all_content, 'Should find the View All tab content' ); + + $this->assertStringContainsString( 'No items.', $view_all_content, 'No items message should be shown' ); + } + + /** + * Test tab navigation structure is rendered correctly. + */ + public function test_tab_navigation_structure() { + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + ) + ); + + $post_type = get_post_type_object( 'page' ); + $box = array( + 'args' => $post_type, + ); + + ob_start(); + wp_nav_menu_item_post_type_meta_box( null, $box ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'posttype-tabs', $output ); + $this->assertStringContainsString( 'Most Recent', $output ); + $this->assertStringContainsString( 'View All', $output ); + $this->assertStringContainsString( 'Search', $output ); + } +}