From 5c76979fb84df72fa93cfe751186e2f39389a614 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2015 23:19:33 +0000 Subject: [PATCH] Customizer: Finish unit tests for nav menus. Removes object_type restriction to allow for future extensibility. Refactors some methods to improve testability. Includes new tests for Ajax requests. Fixes #32687. Props valendesigns, welcher, westonruter. git-svn-id: https://develop.svn.wordpress.org/trunk@33322 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-customize-nav-menus.php | 60 +- tests/phpunit/tests/ajax/CustomizeMenus.php | 538 ++++++++++++++++++ tests/phpunit/tests/customize/nav-menus.php | 162 +++++- 3 files changed, 721 insertions(+), 39 deletions(-) create mode 100644 tests/phpunit/tests/ajax/CustomizeMenus.php diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index b01cf73758..fefacfeb59 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -80,20 +80,38 @@ final class WP_Customize_Nav_Menus { } $obj_type = sanitize_key( $_POST['obj_type'] ); - if ( ! in_array( $obj_type, array( 'post_type', 'taxonomy' ) ) ) { - wp_send_json_error( 'nav_menus_invalid_obj_type' ); - } + $obj_name = sanitize_key( $_POST['type'] ); + $page = empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ); + $items = $this->load_available_items_query( $obj_type, $obj_name, $page ); - $taxonomy_or_post_type = sanitize_key( $_POST['type'] ); - $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0; + if ( is_wp_error( $items ) ) { + wp_send_json_error( $items->get_error_code() ); + } else { + wp_send_json_success( array( 'items' => $items ) ); + } + } + + /** + * Performs the post_type and taxonomy queries for loading available menu items. + * + * @since 4.3.0 + * @access public + * + * @param string $obj_type Optional. Accepts any custom object type and has built-in support for + * 'post_type' and 'taxonomy'. Default is 'post_type'. + * @param string $obj_name Optional. Accepts any registered taxonomy or post type name. Default is 'page'. + * @param int $page Optional. The page number used to generate the query offset. Default is '0'. + * @return WP_Error|array Returns either a WP_Error object or an array of menu items. + */ + public function load_available_items_query( $obj_type = 'post_type', $obj_name = 'page', $page = 0 ) { $items = array(); if ( 'post_type' === $obj_type ) { - if ( ! get_post_type_object( $taxonomy_or_post_type ) ) { - wp_send_json_error( 'nav_menus_invalid_post_type' ); + if ( ! get_post_type_object( $obj_name ) ) { + return new WP_Error( 'nav_menus_invalid_post_type' ); } - if ( 0 === $page && 'page' === $taxonomy_or_post_type ) { + if ( 0 === $page && 'page' === $obj_name ) { // Add "Home" link. Treat as a page, but switch to custom on add. $items[] = array( 'id' => 'home', @@ -110,7 +128,7 @@ final class WP_Customize_Nav_Menus { 'offset' => 10 * $page, 'orderby' => 'date', 'order' => 'DESC', - 'post_type' => $taxonomy_or_post_type, + 'post_type' => $obj_name, ) ); foreach ( $posts as $post ) { $post_title = $post->post_title; @@ -129,7 +147,7 @@ final class WP_Customize_Nav_Menus { ); } } elseif ( 'taxonomy' === $obj_type ) { - $terms = get_terms( $taxonomy_or_post_type, array( + $terms = get_terms( $obj_name, array( 'child_of' => 0, 'exclude' => '', 'hide_empty' => false, @@ -142,7 +160,7 @@ final class WP_Customize_Nav_Menus { 'pad_counts' => false, ) ); if ( is_wp_error( $terms ) ) { - wp_send_json_error( $terms->get_error_code() ); + return $terms; } foreach ( $terms as $term ) { @@ -158,7 +176,7 @@ final class WP_Customize_Nav_Menus { } } - wp_send_json_success( array( 'items' => $items ) ); + return $items; } /** @@ -184,12 +202,12 @@ final class WP_Customize_Nav_Menus { } $s = sanitize_text_field( wp_unslash( $_POST['search'] ) ); - $results = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) ); + $items = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) ); - if ( empty( $results ) ) { - wp_send_json_error( array( 'message' => __( 'No results found.' ) ) ); + if ( empty( $items ) ) { + wp_send_json_error( array( 'message' => __( 'No menu items found.' ) ) ); } else { - wp_send_json_success( array( 'items' => $results ) ); + wp_send_json_success( array( 'items' => $items ) ); } } @@ -202,10 +220,10 @@ final class WP_Customize_Nav_Menus { * @access public * * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments. - * @return array Results. + * @return array Menu items. */ public function search_available_items_query( $args = array() ) { - $results = array(); + $items = array(); $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' ); $query = array( @@ -235,7 +253,7 @@ final class WP_Customize_Nav_Menus { /* translators: %d: ID of a post */ $post_title = sprintf( __( '#%d (no title)' ), $post->ID ); } - $results[] = array( + $items[] = array( 'id' => 'post-' . $post->ID, 'title' => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ), 'type' => 'post_type', @@ -258,7 +276,7 @@ final class WP_Customize_Nav_Menus { // Check if any taxonomies were found. if ( ! empty( $terms ) ) { foreach ( $terms as $term ) { - $results[] = array( + $items[] = array( 'id' => 'term-' . $term->term_id, 'title' => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ), 'type' => 'taxonomy', @@ -270,7 +288,7 @@ final class WP_Customize_Nav_Menus { } } - return $results; + return $items; } /** diff --git a/tests/phpunit/tests/ajax/CustomizeMenus.php b/tests/phpunit/tests/ajax/CustomizeMenus.php new file mode 100644 index 0000000000..c39e9b591e --- /dev/null +++ b/tests/phpunit/tests/ajax/CustomizeMenus.php @@ -0,0 +1,538 @@ +factory->user->create( array( 'role' => 'administrator' ) ) ); + global $wp_customize; + $this->wp_customize = new WP_Customize_Manager(); + $wp_customize = $this->wp_customize; + } + + /** + * Tear down the test fixture. + */ + public function tearDown() { + wp_set_current_user( 0 ); + parent::tearDown(); + } + + /** + * Helper to keep it DRY + * + * @param string $action Action. + */ + protected function make_ajax_call( $action ) { + // Make the request. + try { + $this->_handleAjax( $action ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + } + + /** + * Testing capabilities check for ajax_load_available_items method + * + * @dataProvider data_ajax_load_available_items_cap_check + * + * @param string $role The role we're checking caps against. + * @param array $expected_results Expected results. + */ + function test_ajax_load_available_items_cap_check( $role, $expected_results ) { + + if ( 'administrator' != $role ) { + // If we're not an admin, we should get a wp_die(-1). + $this->setExpectedException( 'WPAjaxDieStopException' ); + } + + wp_set_current_user( $this->factory->user->create( array( 'role' => $role ) ) ); + + $_POST = array( + 'action' => 'load-available-menu-items-customizer', + 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ), + ); + + $this->make_ajax_call( 'load-available-menu-items-customizer' ); + + // If we are an admin, we should get a proper response. + if ( 'administrator' === $role ) { + // Get the results. + $response = json_decode( $this->_last_response, true ); + + $this->assertSame( $expected_results, $response ); + } + + } + + /** + * Data provider for test_ajax_load_available_items_cap_check(). + * + * Provides various post_args to induce error messages in the that can be + * compared to the expected_results. + * + * @since 4.3.0 + * + * @return array { + * @type array { + * @string string $role The role that will test caps for. + * @array array $expected_results The expected results from the ajax call. + * } + * } + */ + function data_ajax_load_available_items_cap_check() { + return array( + array( + 'subscriber', + array(), + ), + array( + 'contributor', + array(), + ), + array( + 'author', + array(), + ), + array( + 'editor', + array(), + ), + array( + 'administrator', + array( + 'success' => false, + 'data' => 'nav_menus_missing_obj_type_or_type_parameter', + ), + ), + ); + } + + /** + * Testing the error messaging for ajax_load_available_items + * + * @dataProvider data_ajax_load_available_items_error_messages + * + * @param array $post_args POST args. + * @param mixed $expected_results Expected results. + */ + function test_ajax_load_available_items_error_messages( $post_args, $expected_results ) { + + $_POST = array_merge( array( + 'action' => 'load-available-menu-items-customizer', + 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ), + ), $post_args ); + + // Make the request. + $this->make_ajax_call( 'load-available-menu-items-customizer' ); + + // Get the results. + $response = json_decode( $this->_last_response, true ); + + $this->assertSame( $expected_results, $response ); + } + + /** + * Data provider for test_ajax_load_available_items_error_message(). + * + * Provides various post_args to induce error messages in the that can be + * compared to the expected_results. + * + * @since 4.3.0 + * + * @return array { + * @type array { + * @array array $post_args The arguments that will merged with the $_POST array. + * @array array $expected_results The expected results from the ajax call. + * } + * } + */ + function data_ajax_load_available_items_error_messages() { + return array( + // Testing empty obj_type and type. + array( + array( + 'obj_type' => '', + 'type' => '', + ), + array( + 'success' => false, + 'data' => 'nav_menus_missing_obj_type_or_type_parameter', + ), + ), + // Testing empty obj_type. + array( + array( + 'obj_type' => '', + 'type' => 'post', + ), + array( + 'success' => false, + 'data' => 'nav_menus_missing_obj_type_or_type_parameter', + ), + ), + // Testing empty type. + array( + array( + 'obj_type' => '', + 'type' => 'post', + ), + array( + 'success' => false, + 'data' => 'nav_menus_missing_obj_type_or_type_parameter', + ), + ), + // Testing incorrect type option. + array( + array( + 'obj_type' => 'post_type', + 'type' => 'invalid', + ), + array( + 'success' => false, + 'data' => 'nav_menus_invalid_post_type', + ), + ), + ); + } + + /** + * Testing the success status. + * + * @dataProvider data_ajax_load_available_items_success_status + * + * @param array $post_args POST args. + * @param array $success_status Success status. + */ + function test_ajax_load_available_items_success_status( $post_args, $success_status ) { + + $_POST = array_merge( array( + 'action' => 'load-available-menu-items-customizer', + 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ), + ), $post_args ); + + // Make the request. + $this->make_ajax_call( 'load-available-menu-items-customizer' ); + + // Get the results. + $response = json_decode( $this->_last_response, true ); + $this->assertSame( $success_status, $response['success'] ); + + } + + /** + * Data provider for test_ajax_load_available_items_success_status(). + * + * Provides various post_args to retrieve results and compare against + * the success status. + * + * @since 4.3.0 + * + * @return array { + * @type array { + * @type array $post_args The arguments that will merged with the $_POST array. + * @type bool $success_status The expected success status. + * } + * } + */ + function data_ajax_load_available_items_success_status() { + return array( + array( + array( + 'obj_type' => 'post_type', + 'type' => 'post', + ), + true, + ), + array( + array( + 'obj_type' => 'post_type', + 'type' => 'page', + ), + true, + ), + array( + array( + 'obj_type' => 'post_type', + 'type' => 'custom', + ), + false, + ), + array( + array( + 'obj_type' => 'taxonomy', + 'type' => 'post_tag', + ), + true, + ), + ); + } + + /** + * Testing the array structure for a single item + * + * @dataProvider data_ajax_load_available_items_structure + * + * @param array $post_args POST args. + */ + function test2_ajax_load_available_items_structure( $post_args ) { + + $expected_keys = array( + 'id', + 'title', + 'type', + 'type_label', + 'object', + 'object_id', + 'url', + ); + + // Create some terms and pages. + $this->factory->term->create_many( 5 ); + $this->factory->post->create_many( 5, array( 'post_type' => 'page' ) ); + + $_POST = array_merge( array( + 'action' => 'load-available-menu-items-customizer', + 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ), + ), $post_args ); + + // Make the request. + $this->make_ajax_call( 'load-available-menu-items-customizer' ); + + // Get the results. + $response = json_decode( $this->_last_response, true ); + + $this->assertNotEmpty( $response['data']['items'] ); + + // Get the second index to avoid the home page edge case. + $test_item = $response['data']['items'][1]; + + foreach ( $expected_keys as $key ) { + $this->assertArrayHasKey( $key, $test_item ); + $this->assertNotEmpty( $test_item[ $key ] ); + } + + // Special test for the home page. + if ( 'page' === $test_item['object'] ) { + $home = $response['data']['items'][0]; + foreach ( $expected_keys as $key ) { + if ( 'object_id' !== $key ) { + $this->assertArrayHasKey( $key, $home ); + if ( 'object' !== $key ) { + $this->assertNotEmpty( $home[ $key ] ); + } + } + } + } + } + + /** + * Data provider for test_ajax_load_available_items_structure(). + * + * Provides various post_args to return a list of items to test the array structure of. + * + * @since 4.3.0 + * + * @return array { + * @type array { + * @type array $post_args The arguments that will merged with the $_POST array. + * } + * } + */ + function data_ajax_load_available_items_structure() { + return array( + array( + array( + 'obj_type' => 'post_type', + 'type' => 'post', + ), + ), + array( + array( + 'obj_type' => 'post_type', + 'type' => 'page', + ), + ), + array( + array( + 'obj_type' => 'taxonomy', + 'type' => 'post_tag', + ), + ), + ); + } + + /** + * Testing the error messages for ajax_search_available_items + * + * @dataProvider data_ajax_search_available_items_caps_check + * + * @param string $role Role. + * @param array $expected_results Expected results. + */ + function test_ajax_search_available_items_caps_check( $role, $expected_results ) { + + if ( 'administrator' != $role ) { + // If we're not an admin, we should get a wp_die(-1). + $this->setExpectedException( 'WPAjaxDieStopException' ); + } + + wp_set_current_user( $this->factory->user->create( array( 'role' => $role ) ) ); + + $_POST = array( + 'action' => 'search-available-menu-items-customizer', + 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ), + ); + + $this->make_ajax_call( 'search-available-menu-items-customizer' ); + + // If we are an admin, we should get a proper response. + if ( 'administrator' === $role ) { + // Get the results. + $response = json_decode( $this->_last_response, true ); + + $this->assertSame( $expected_results, $response ); + } + } + + /** + * Data provider for test_ajax_search_available_items_caps_check(). + * + * Provides various post_args to induce error messages in the that can be + * compared to the expected_results. + * + * @since 4.3.0 + * + * @todo Make this more DRY + * + * @return array { + * @type array { + * @string string $role The role that will test caps for. + * @array array $expected_results The expected results from the ajax call. + * } + * } + */ + function data_ajax_search_available_items_caps_check() { + return array( + array( + 'subscriber', + array(), + ), + array( + 'contributor', + array(), + ), + array( + 'author', + array(), + ), + array( + 'editor', + array(), + ), + array( + 'administrator', + array( + 'success' => false, + 'data' => 'nav_menus_missing_search_parameter', + ), + ), + ); + } + + /** + * Testing the results of various searches + * + * @dataProvider data_ajax_search_available_items_results + * + * @param array $post_args POST args. + * @param array $expected_results Expected results. + */ + function test_ajax_search_available_items_results( $post_args, $expected_results ) { + + $this->factory->post->create_many( 5, array( 'post_title' => 'Test Post' ) ); + + $_POST = array_merge( array( + 'action' => 'search-available-menu-items-customizer', + 'customize-menus-nonce' => wp_create_nonce( 'customize-menus' ), + ), $post_args ); + + $this->make_ajax_call( 'search-available-menu-items-customizer' ); + + $response = json_decode( $this->_last_response, true ); + + if ( isset( $post_args['search'] ) && 'test' === $post_args['search'] ) { + $this->assertsame( true, $response['success'] ); + $this->assertSame( 5, count( $response['data']['items'] ) ); + } else { + $this->assertSame( $expected_results, $response ); + } + + } + + /** + * Data provider for test_ajax_search_available_items_results(). + * + * Provides various post_args to test the results. + * + * @since 4.3.0 + * + * @return array { + * @type array { + * @string string $post_args The args that will be passed to ajax. + * @array array $expected_results The expected results from the ajax call. + * } + * } + */ + function data_ajax_search_available_items_results() { + return array( + array( + array(), + array( + 'success' => false, + 'data' => 'nav_menus_missing_search_parameter', + ), + ), + array( + array( + 'search' => 'all_the_things', + ), + array( + 'success' => false, + 'data' => array( + 'message' => 'No menu items found.', + ), + ), + ), + array( + array( + 'search' => 'test', + ), + array( + 'success' => true, + array(), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/customize/nav-menus.php b/tests/phpunit/tests/customize/nav-menus.php index 7fa88e7d62..41991a82f0 100644 --- a/tests/phpunit/tests/customize/nav-menus.php +++ b/tests/phpunit/tests/customize/nav-menus.php @@ -49,25 +49,161 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { } /** - * Test the test_load_available_items_ajax method. + * Test that the load_available_items_query method returns a WP_Error object. * - * @see WP_Customize_Nav_Menus::load_available_items_ajax() + * @see WP_Customize_Nav_Menus::load_available_items_query() */ - function test_load_available_items_ajax() { + function test_load_available_items_query_returns_wp_error() { + $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); - $this->markTestIncomplete( 'This test has not been implemented.' ); + // Invalid post type $obj_name. + $items = $menus->load_available_items_query( 'post_type', 'invalid' ); + $this->assertInstanceOf( 'WP_Error', $items ); + $this->assertEquals( 'nav_menus_invalid_post_type', $items->get_error_code() ); + // Invalid taxonomy $obj_name. + $items = $menus->load_available_items_query( 'taxonomy', 'invalid' ); + $this->assertInstanceOf( 'WP_Error', $items ); + $this->assertEquals( 'invalid_taxonomy', $items->get_error_code() ); } /** - * Test the search_available_items_ajax method. + * Test the load_available_items_query method maybe returns the home page item. * - * @see WP_Customize_Nav_Menus::search_available_items_ajax() + * @see WP_Customize_Nav_Menus::load_available_items_query() */ - function test_search_available_items_ajax() { + function test_load_available_items_query_maybe_returns_home() { + $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); - $this->markTestIncomplete( 'This test has not been implemented.' ); + // Expected menu item array. + $expected = array( + 'id' => 'home', + 'title' => _x( 'Home', 'nav menu home label' ), + 'type' => 'custom', + 'type_label' => __( 'Custom Link' ), + 'object' => '', + 'url' => home_url(), + ); + // Create pages. + $this->factory->post->create_many( 15, array( 'post_type' => 'page' ) ); + + // Home is included in menu items when page is zero. + $items = $menus->load_available_items_query( 'post_type', 'page', 0 ); + $this->assertContains( $expected, $items ); + + // Home is not included in menu items when page is larger than zero. + $items = $menus->load_available_items_query( 'post_type', 'page', 1 ); + $this->assertNotEmpty( $items ); + $this->assertNotContains( $expected, $items ); + } + + /** + * Test the load_available_items_query method returns post item. + * + * @see WP_Customize_Nav_Menus::load_available_items_query() + */ + function test_load_available_items_query_returns_post_item_with_page_number() { + $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); + + // Create page. + $post_id = $this->factory->post->create( array( 'post_title' => 'Post Title' ) ); + + // Create pages. + $this->factory->post->create_many( 10 ); + + // Expected menu item array. + $expected = array( + 'id' => "post-{$post_id}", + 'title' => 'Post Title', + 'type' => 'post_type', + 'type_label' => 'Post', + 'object' => 'post', + 'object_id' => intval( $post_id ), + 'url' => get_permalink( intval( $post_id ) ), + ); + + // Offset the query and get the second page of menu items. + $items = $menus->load_available_items_query( 'post_type', 'post', 1 ); + $this->assertContains( $expected, $items ); + } + + /** + * Test the load_available_items_query method returns page item. + * + * @see WP_Customize_Nav_Menus::load_available_items_query() + */ + function test_load_available_items_query_returns_page_item() { + $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); + + // Create page. + $page_id = $this->factory->post->create( array( 'post_title' => 'Page Title', 'post_type' => 'page' ) ); + + // Expected menu item array. + $expected = array( + 'id' => "post-{$page_id}", + 'title' => 'Page Title', + 'type' => 'post_type', + 'type_label' => 'Page', + 'object' => 'page', + 'object_id' => intval( $page_id ), + 'url' => get_permalink( intval( $page_id ) ), + ); + + $items = $menus->load_available_items_query( 'post_type', 'page', 0 ); + $this->assertContains( $expected, $items ); + } + + /** + * Test the load_available_items_query method returns post item. + * + * @see WP_Customize_Nav_Menus::load_available_items_query() + */ + function test_load_available_items_query_returns_post_item() { + $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); + + // Create post. + $post_id = $this->factory->post->create( array( 'post_title' => 'Post Title' ) ); + + // Expected menu item array. + $expected = array( + 'id' => "post-{$post_id}", + 'title' => 'Post Title', + 'type' => 'post_type', + 'type_label' => 'Post', + 'object' => 'post', + 'object_id' => intval( $post_id ), + 'url' => get_permalink( intval( $post_id ) ), + ); + + $items = $menus->load_available_items_query( 'post_type', 'post', 0 ); + $this->assertContains( $expected, $items ); + } + + /** + * Test the load_available_items_query method returns term item. + * + * @see WP_Customize_Nav_Menus::load_available_items_query() + */ + function test_load_available_items_query_returns_term_item() { + $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); + + // Create term. + $term_id = $this->factory->category->create( array( 'name' => 'Term Title' ) ); + + // Expected menu item array. + $expected = array( + 'id' => "term-{$term_id}", + 'title' => 'Term Title', + 'type' => 'taxonomy', + 'type_label' => 'Category', + 'object' => 'category', + 'object_id' => intval( $term_id ), + 'url' => get_term_link( intval( $term_id ), 'category' ), + ); + + $items = $menus->load_available_items_query( 'taxonomy', 'category', 0 ); + $this->assertContains( $expected, $items ); } /** @@ -454,14 +590,4 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { } - /** - * Test the render_menu method. - * - * @see WP_Customize_Nav_Menus::render_menu() - */ - function test_render_menu() { - - $this->markTestIncomplete( 'This test has not been implemented.' ); - } - }