diff --git a/.travis.yml b/.travis.yml index 996f521..c3c7eca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ branches: php: - 5.6 - 7.0 + - 7.1 + - 7.2 - nightly env: @@ -24,9 +26,6 @@ matrix: - dist: precise php: 5.3 env: WP_VERSION=latest WP_MULTISITE=0 - - dist: precise - php: 5.3 - env: WP_VERSION=latest WP_MULTISITE=1 allow_failures: - php: nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index e437055..3be87d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ #CHANGELOG +## [4.0.2] +- Fix dropdown opener having empty string for href value. +- More accurate regex matching of icon and linkmod classnames. +- Changed composer package type to `library` from `wordpress-plugin` again. +- Tests: Add unit tests for the function that separates classnames for the walker. +- Fix case sensitive matching to now match mixes of upper and lower case. ## [4.0.1] - Fix untranslated string in fallback (this was lost in transition between v3 and v4, fixed again). diff --git a/class-wp-bootstrap-navwalker.php b/class-wp-bootstrap-navwalker.php index e3ffe94..34b9578 100644 --- a/class-wp-bootstrap-navwalker.php +++ b/class-wp-bootstrap-navwalker.php @@ -11,7 +11,7 @@ * Plugin URI: https://github.com/wp-bootstrap/wp-bootstrap-navwalker * Description: A custom WordPress nav walker class to implement the Bootstrap 4 navigation style in a custom theme using the WordPress built in menu manager. * Author: Edward McIntyre - @twittem, WP Bootstrap, William Patton - @pattonwebz - * Version: 4.0.1 + * Version: 4.0.2 * Author URI: https://github.com/wp-bootstrap * GitHub Plugin URI: https://github.com/wp-bootstrap/wp-bootstrap-navwalker * GitHub Branch: master @@ -189,7 +189,7 @@ public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 $atts['class'] = 'dropdown-toggle nav-link'; $atts['id'] = 'menu-item-dropdown-' . $item->ID; } else { - $atts['href'] = ! empty( $item->url ) ? $item->url : ''; + $atts['href'] = ! empty( $item->url ) ? $item->url : '#'; // Items in dropdowns use .dropdown-item instead of .nav-link. if ( $depth > 0 ) { $atts['class'] = 'dropdown-item'; @@ -377,7 +377,7 @@ public static function fallback( $args ) { } else { return $fallback_output; } - } // End if(). + } } /** @@ -403,20 +403,20 @@ private function seporate_linkmods_and_icons_from_classes( $classes, &$linkmod_c foreach ( $classes as $key => $class ) { // If any special classes are found, store the class in it's // holder array and and unset the item from $classes. - if ( preg_match( '/disabled|sr-only/', $class ) ) { + if ( preg_match( '/^disabled|^sr-only/i', $class ) ) { // Test for .disabled or .sr-only classes. $linkmod_classes[] = $class; unset( $classes[ $key ] ); - } elseif ( preg_match( '/dropdown-header|dropdown-divider/', $class ) && $depth > 0 ) { + } elseif ( preg_match( '/^dropdown-header|^dropdown-divider/i', $class ) && $depth > 0 ) { // Test for .dropdown-header or .dropdown-divider and a // depth greater than 0 - IE inside a dropdown. $linkmod_classes[] = $class; unset( $classes[ $key ] ); - } elseif ( preg_match( '/fa-(\S*)?|fas(\s?)|far(\s?)|fal(\s?)|fab(\s?)|fa(\s?)/', $class ) ) { + } elseif ( preg_match( '/^fa-(\S*)?|^fa(s|r|l|b)?(\s?)?$/i', $class ) ) { // Font Awesome. $icon_classes[] = $class; unset( $classes[ $key ] ); - } elseif ( preg_match( '/glyphicons-(\S*)?|glyphicons(\s?)/', $class ) ) { + } elseif ( preg_match( '/^glyphicon-(\S*)?|^glyphicon(\s?)$/i', $class ) ) { // Glyphicons. $icon_classes[] = $class; unset( $classes[ $key ] ); @@ -550,4 +550,4 @@ private function linkmod_element_close( $linkmod_type ) { return $output; } } -} // End if(). +} diff --git a/composer.json b/composer.json index a58a514..40607c5 100644 --- a/composer.json +++ b/composer.json @@ -1,21 +1,24 @@ { "name": "wp-bootstrap/wp-bootstrap-navwalker", "description": "A custom WordPress nav walker class to fully implement the Bootstrap 4 navigation style in a custom theme using the WordPress built in menu manager.", - "type": "wordpress-plugin", + "type": "library", "license": "GPL-3.0+", "authors": [ { "name": "Brandon Hubbard" }, - { - "name": "William Patton", - "email": "will@pattonwebz.com" - } + { + "name": "William Patton", + "email": "will@pattonwebz.com" + } ], "support": { "issues": "https://github.com/wp-bootstrap/wp-bootstrap-navwalker/issues/", "source": "https://github.com/wp-bootstrap/wp-bootstrap-navwalker/" }, + "autoload": { + "files": ["class-wp-bootstrap-navwalker.php"] + }, "require": { "composer/installers": "~1.0" }, diff --git a/tests/test-navwalker.php b/tests/test-navwalker.php index 19b7aed..7fb49a2 100644 --- a/tests/test-navwalker.php +++ b/tests/test-navwalker.php @@ -3,6 +3,8 @@ * Class SampleTest * * @package Wp_Bootstrap_Navwalker + * + * phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r -- used for returned failure messages to give some details. */ /** @@ -11,14 +13,13 @@ * @extends WP_UnitTestCase */ class Test_WP_Bootstrap_NavWalker extends WP_UnitTestCase { - /** * The setUp function. * * @access public * @return void */ - function setUp() { + public function setUp() { parent::setUp(); @@ -31,14 +32,56 @@ function setUp() { 'container_class' => 'a_container_class', 'menu_class' => 'a_menu_class', 'menu_id' => 'a_menu_id', - 'echo' => true, + 'echo' => true, ); - // array of the possible linkmods. + // array of the possible linkmod typeflags. $this->valid_linkmod_typeflags = array( 'dropdown-header', 'dropdown-divider', ); + + // array of all possible linkmods, including the valid typeflags. + $this->valid_linkmod_classes = array_merge( $this->valid_linkmod_typeflags, array( + 'disabled', + 'sr-only', + ) ); + + // array of valid font-awesome icon class starters plus some randomly + // chosen icon classes and some variations of upper/lower case letters. + $this->some_fontawesome_classes = array( + 'fa', + 'fas', + 'fab', + 'far', + 'fal', + 'fa-android', + 'fa-css3', + 'fa-home', + 'fa-bluetooth-b', + 'fa-chess-rook', + 'fA-home', + 'Fa-HoMe', + 'fa-HomE', + ); + + // array of valid glyphicon icon class starters plus some randomly + // chosen icon classes and some variations of upper/lower case letters. + $this->some_glyphicons_classes = array( + 'glyphicon', + 'glyphicon-asterisk', + 'glyphicon-ok', + 'glyphicon-file', + 'glyphicon-hand-left', + 'glyphicon-sd-video', + 'glyphicon-subscript', + 'glyphicon-grain', + 'Glyphicon-file', + 'Glyphicon-File', + 'glyphicon-File', + 'glYphiCon-fiLe', + ); + } /** @@ -47,7 +90,7 @@ function setUp() { * @access public * @return void */ - function test_navwalker_file_exists() { + public function test_navwalker_file_exists() { $this->assertFileExists( 'class-wp-bootstrap-navwalker.php' ); } @@ -57,7 +100,7 @@ function test_navwalker_file_exists() { * @access public * @return void */ - function test_startlvl_function_exists() { + public function test_startlvl_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -74,7 +117,7 @@ function test_startlvl_function_exists() { * @access public * @return void */ - function test_start_el_function_exists() { + public function test_start_el_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -91,7 +134,7 @@ function test_start_el_function_exists() { * @access public * @return void */ - function test_display_element_function_exists() { + public function test_display_element_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -103,12 +146,12 @@ function test_display_element_function_exists() { } /** - * Test Fallback Function exists. + * Test Fallback function exists. * * @access public * @return void */ - function test_fallback_function_exists() { + public function test_fallback_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -124,8 +167,11 @@ function test_fallback_function_exists() { * * Expects that for logged out users both echo and return requests should * produce empty strings. + * + * @access public + * @return void */ - function test_fallback_function_output_loggedout() { + public function test_fallback_function_output_loggedout() { // default is to echo reults, buffer. ob_start(); @@ -156,8 +202,11 @@ function test_fallback_function_output_loggedout() { * * Expects strings to be produced with html markup and that they match when * requesting either a return or defaulting to echo. + * + * @access public + * @return void */ - function test_fallback_function_output_loggedin() { + public function test_fallback_function_output_loggedin() { // make an admin user and set it to be the current user. $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); @@ -170,7 +219,7 @@ function test_fallback_function_output_loggedin() { // rudimentary content test - confirm it opens a div with 2 expected // values and ends by closing a div. - $match = ( preg_match('/^(
)(.*?)(<\/div>)$/', $fallback_output_echo ) ) ? true : false; + $match = ( preg_match( '/^(
)(.*?)(<\/div>)$/', $fallback_output_echo ) ) ? true : false; $this->assertTrue( $match, 'Fallback method seems to create unexpected html for logged in users in echo mode.' @@ -195,7 +244,7 @@ function test_fallback_function_output_loggedin() { * @access public * @return void */ - function test_seporate_linkmods_and_icons_from_classes_function_exists() { + public function test_seporate_linkmods_and_icons_from_classes_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -206,13 +255,259 @@ function test_seporate_linkmods_and_icons_from_classes_function_exists() { } + /** + * Test that the function catches a random assortment of glyphicon icon + * classes mixed with with regular classnames. + * + * @depends test_seporate_linkmods_and_icons_from_classes_function_exists + * + * @access public + * @return void + */ + public function test_seporate_linkmods_and_icons_from_classes_fontawesome() { + + $wp_bootstrap_navwalker = $this->walker; + // since we're working with private methods we need to use a reflector. + $reflector = new ReflectionClass( 'WP_Bootstrap_Navwalker' ); + + // get a reflected method for the opener function and set to public. + $method_open = $reflector->getMethod( 'seporate_linkmods_and_icons_from_classes' ); + $method_open->setAccessible( true ); + + $icons_array = $this->some_fontawesome_classes; + $linkmod_classes = array(); + $icon_classes = array(); + $chars = range( 'a', 'z' ); + $extra_classes = array(); + // make 10 random valid classnames with legth of 8 chars (there should + // be no naming collisions here with this random string gen method). + for ( $i = 0; $i < 20; $i++ ) { + $string = ''; + $length = mt_rand( 4, 10 ); + for ( $j = 0; $j < $length; $j++ ) { + $string .= $chars[ mt_rand( 0, count( $chars ) - 1 ) ]; + } + $extra_classes[] = $string; + } + // merge the valid icon classes with the extra classes and shuffle order. + $icons_array = array_merge( $icons_array, $extra_classes ); + shuffle( $icons_array ); + + $returned_array = $method_open->invokeArgs( $wp_bootstrap_navwalker, array( $icons_array, &$linkmod_classes, &$icon_classes, 0 ) ); + + // linkmod_classes should be empty and returned_array should not. + $this->assertTrue( ( empty( $linkmod_classes ) && ! empty( $returned_array ) ) ); + // icon_classes should no longer be empty. + $this->assertNotTrue( empty( $icon_classes ) ); + // the number of items inside updated $icon_classes should match number of valids we started with. + $this->assertTrue( count( $this->some_fontawesome_classes ) === count( $icon_classes ), "Seems that glyphicons classes are not catptured properly... \nvalid: \n" . print_r( $this->some_fontawesome_classes, true ) . "\nreturned: \n" . print_r( $icon_classes, true ) ); + // get the differences between the original classes and updated classes. + $icon_differences = array_diff( $this->some_fontawesome_classes, $icon_classes ); + // should be no differences thus empty array, this being TRUE also means + // that text was exact match in the updated array vs the original. + $this->assertTrue( empty( $icon_differences ) ); + + } + + /** + * Test that the function catches a random assortment of font awesome icon + * classes mixed with with regular classnames. + * + * @depends test_seporate_linkmods_and_icons_from_classes_function_exists + * + * @access public + * @return void + */ + public function test_seporate_linkmods_and_icons_from_classes_glyphicons() { + + $wp_bootstrap_navwalker = $this->walker; + // since we're working with private methods we need to use a reflector. + $reflector = new ReflectionClass( 'WP_Bootstrap_Navwalker' ); + + // get a reflected method for the opener function and set to public. + $method_open = $reflector->getMethod( 'seporate_linkmods_and_icons_from_classes' ); + $method_open->setAccessible( true ); + + $icons_array = $this->some_glyphicons_classes; + $linkmod_classes = array(); + $icon_classes = array(); + $chars = range( 'a', 'z' ); + $extra_classes = array(); + // make 10 random valid classnames with legth of 8 chars (there should + // be no naming collisions here with this random string gen method). + for ( $i = 0; $i < 10; $i++ ) { + $string = ''; + $length = mt_rand( 4, 10 ); + for ( $j = 0; $j < $length; $j++ ) { + $string .= $chars[ mt_rand( 0, count( $chars ) - 1 ) ]; + } + $extra_classes[] = $string; + } + // merge the valid icon classes with the extra classes and shuffle order. + $icons_array = array_merge( $icons_array, $extra_classes ); + shuffle( $icons_array ); + + $returned_array = $method_open->invokeArgs( $wp_bootstrap_navwalker, array( $icons_array, &$linkmod_classes, &$icon_classes, 0 ) ); + + // linkmod_classes should be empty and returned_array should not. + $this->assertTrue( ( empty( $linkmod_classes ) && ! empty( $returned_array ) ) ); + // icon_classes should no longer be empty. + $this->assertNotTrue( empty( $icon_classes ) ); + // the number of items inside updated $icon_classes should match number of valids we started with. + $this->assertTrue( count( $this->some_glyphicons_classes ) === count( $icon_classes ), "Seems that glyphicons classes are not catptured properly... \nvalid: \n" . print_r( $this->some_glyphicons_classes, true ) . "\nreturned: \n" . print_r( $icon_classes, true ) ); + // get the differences between the original classes and updated classes. + $icon_differences = array_diff( $this->some_glyphicons_classes, $icon_classes ); + // should be no differences thus empty array, this being TRUE also means + // that text was exact match in the updated array vs the original. + $this->assertTrue( empty( $icon_differences ) ); + + } + + /** + * Test that the function catches a random assortment of font awesome icon + * classes mixed with with regular classnames. + * + * @depends test_seporate_linkmods_and_icons_from_classes_function_exists + * + * @access public + * @return void + */ + public function test_seporate_linkmods_and_icons_from_classes_linkmods() { + + $wp_bootstrap_navwalker = $this->walker; + // since we're working with private methods we need to use a reflector. + $reflector = new ReflectionClass( 'WP_Bootstrap_Navwalker' ); + + // get a reflected method for the opener function and set to public. + $method_open = $reflector->getMethod( 'seporate_linkmods_and_icons_from_classes' ); + $method_open->setAccessible( true ); + + $valid_linkmods = $this->valid_linkmod_classes; + $linkmod_classes = array(); + $icon_classes = array(); + $chars = range( 'a', 'z' ); + $extra_classes = array(); + // make 20 random valid classnames with legth of 4 to 10 chars. There + // should be no naming collisions here with this random string gen. + for ( $i = 0; $i < 10; $i++ ) { + $string = ''; + $length = mt_rand( 4, 10 ); + for ( $j = 0; $j < $length; $j++ ) { + $string .= $chars[ mt_rand( 0, count( $chars ) - 1 ) ]; + } + $extra_classes[] = $string; + } + // merge the valid icon classes with the extra classes and shuffle order. + $linkmod_array = array_merge( $valid_linkmods, $extra_classes ); + shuffle( $linkmod_array ); + + // NOTE: this is depth of 0 and meaning valid_linkmod_typeflags won't be captured. + $returned_array = $method_open->invokeArgs( $wp_bootstrap_navwalker, array( $linkmod_array, &$linkmod_classes, &$icon_classes, 0 ) ); + + // linkmod_classes should NOT be empty and returned_array should not. + $this->assertTrue( ( ! empty( $linkmod_classes ) && ! empty( $returned_array ) ) ); + // icon_classes should be empty. + $this->assertTrue( empty( $icon_classes ) ); + + $num_of_items_left = count( $this->valid_linkmod_classes ) - count( $linkmod_classes ) - count( $this->valid_linkmod_typeflags ); + // the number of items inside updated array should match [what we started with - minus the linkmods for inside dropdowns]. + $this->assertNotTrue( + (bool) $num_of_items_left, + "Seems that the linkmod classes are not catptured properly when outside of dropdowns... \nvalid: \n" . print_r( $this->valid_linkmod_classes, true ) . "\nreturned: \n" . print_r( $linkmod_classes, true ) + ); + // get the differences between the original classes and updated classes. + $linkmod_differences = array_diff( $this->valid_linkmod_classes, $linkmod_classes, $this->valid_linkmod_typeflags ); + + // should be no differences thus empty array, this being TRUE also means + // that text was exact match in the updated array vs the original. + $this->assertTrue( empty( $linkmod_differences ) ); + + // repeat some of the above tests but this time with depth = 1 so that we catch classes intended for inside dropdowns. + $depth = 1; + $linkmod_classes_d = array(); + $icon_classes_d = array(); + $returned_array_d = $method_open->invokeArgs( $wp_bootstrap_navwalker, array( $linkmod_array, &$linkmod_classes_d, &$icon_classes_d, $depth ) ); + + $this->assertTrue( count( $this->valid_linkmod_classes ) === count( $linkmod_classes_d ), "Seems that the linkmod classes are not catptured properly when inside dropdowns... \nvalid: \n" . print_r( $this->valid_linkmod_classes, true ) . "\nreturned: \n" . print_r( $linkmod_classes, true ) ); + $linkmod_differences_d = array_diff( $this->valid_linkmod_classes, $linkmod_classes_d ); + $this->assertTrue( empty( $linkmod_differences_d ), 'There are differences between the matched classnames and the valid classnames.' ); + + } + + /** + * Test that the function catches all possible linkmod classes, any icon + * classes and leaves the other classes as-is on the array. + * + * @depends test_seporate_linkmods_and_icons_from_classes_function_exists + * + * @depends test_seporate_linkmods_and_icons_from_classes_fontawesome + * @depends test_seporate_linkmods_and_icons_from_classes_glyphicons + * @depends test_seporate_linkmods_and_icons_from_classes_linkmods + * + * @access public + * @return void + */ + public function test_seporate_linkmods_and_icons_from_classes_fulltest() { + + $wp_bootstrap_navwalker = $this->walker; + // since we're working with private methods we need to use a reflector. + $reflector = new ReflectionClass( 'WP_Bootstrap_Navwalker' ); + + // get a reflected method for the opener function and set to public. + $method_open = $reflector->getMethod( 'seporate_linkmods_and_icons_from_classes' ); + $method_open->setAccessible( true ); + + $icons_array = array_merge( $this->some_fontawesome_classes, $this->some_glyphicons_classes ); + $linkmod_array = $this->valid_linkmod_classes; + $linkmod_classes = array(); + $icon_classes = array(); + $chars = range( 'a', 'z' ); + $extra_classes = array(); + // make 1000 random valid classnames with legth of 8 chars (there should + // be no naming collisions here with this random string gen method). + for ( $i = 0; $i < 1000; $i++ ) { + $string = ''; + $length = mt_rand( 4, 10 ); + for ( $j = 0; $j < $length; $j++ ) { + $string .= $chars[ mt_rand( 0, count( $chars ) - 1 ) ]; + } + $extra_classes[] = $string; + } + // merge the valid icon classes with valid linkmod classes and the extra classes then shuffle order. + $classname_array = array_merge( $icons_array, $linkmod_array, $extra_classes ); + shuffle( $classname_array ); + + // need a depth of 1 to ensure that our linkmods classes for inside dropdowns are also captured. + $depth = 1; + $returned_array = $method_open->invokeArgs( $wp_bootstrap_navwalker, array( $classname_array, &$linkmod_classes, &$icon_classes, $depth ) ); + + // linkmod_classes NOT should be empty and returned_array should not. + $this->assertTrue( ( ! empty( $linkmod_classes ) && ! empty( $returned_array ) ), 'Either the linkmod array or the returned non matching classes array is empty when they shoud not be.' ); + // starting arrays should no longer be empty. + $this->assertNotTrue( empty( $icon_classes ), 'Did not catch any icons.' ); + $this->assertNotTrue( empty( $linkmod_classes ), 'Did not catch any linkmods.' ); + + // icons compair. + $this->assertTrue( count( $icons_array ) === count( $icon_classes ), "Seems that icon classes are not catptured properly... valid: \n" . print_r( $icons_array, true ) . "returned: \n" . print_r( $icon_classes, true ) ); + $icon_differences = array_diff( $icons_array, $icon_classes ); + $this->assertTrue( empty( $icon_differences ), 'Seems that we did not catch all of the icon classes.' ); + // linkmod compair. + $this->assertTrue( count( $linkmod_array ) === count( $linkmod_classes ), "Seems that linkmod classes are not catptured properly... valid: \n" . print_r( $linkmod_array, true ) . "returned: \n" . print_r( $linkmod_classes, true ) ); + $linkmod_differences = array_diff( $icons_array, $icon_classes ); + $this->assertTrue( empty( $linkmod_differences ), 'Seems that we did not catch all of the linkmod classes.' ); + // extra classes string matches checks. + $returned_differences = array_diff( $returned_array, $extra_classes ); + $this->assertTrue( empty( $returned_differences ), 'The returned array minus the extra classes should be empty, likely some classes were missed or string malformation occured.' ); + + } + /** * Test get_linkmod_type Function exists. * * @access public * @return void */ - function test_get_linkmod_type_function_exists() { + public function test_get_linkmod_type_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -229,7 +524,7 @@ function test_get_linkmod_type_function_exists() { * @access public * @return void */ - function test_update_atts_for_linkmod_type_function_exists() { + public function test_update_atts_for_linkmod_type_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -246,7 +541,7 @@ function test_update_atts_for_linkmod_type_function_exists() { * @access public * @return void */ - function test_linkmod_element_open_function_exists() { + public function test_linkmod_element_open_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -263,7 +558,7 @@ function test_linkmod_element_open_function_exists() { * @access public * @return void */ - function test_linkmod_element_close_function_exists() { + public function test_linkmod_element_close_function_exists() { $wp_bootstrap_navwalker = $this->walker; @@ -274,7 +569,14 @@ function test_linkmod_element_close_function_exists() { } - function test_linkmod_elements_open_and_close_successfully() { + /** + * Tests for valid markup being used as the opener and closer sections for + * some different linkmod types. + * + * @access public + * @return void + */ + public function test_linkmod_elements_open_and_close_successfully() { $wp_bootstrap_navwalker = $this->walker;