Skip to content

Instantly share code, notes, and snippets.

@davilera
Last active December 29, 2016 09:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davilera/f2e0a9c54cc9c13e21a4599a938b4a4b to your computer and use it in GitHub Desktop.
Save davilera/f2e0a9c54cc9c13e21a4599a938b4a4b to your computer and use it in GitHub Desktop.
Unit Testing AJAX

Unit Testing AJAX calls in WordPress

<?php
/**
* The plugin bootstrap file.
*
* Plugin Name: Nelio Post Searcher
* Description: Example.
* Version: 1.0.0
*
* Author: Nelio Software
* Author URI: https://neliosoftware.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
*
* @package Nelio_Post_Searcher
* @subpackage Root
* @author David Aguilera <david.aguilera@neliosoftware.com>
* @since 1.0.0
*/
<?php
/**
* Adds the search form at the end of a post.
*
* @param string $where The content of the current post.
*
* @return string the post with the new form.
*
* @since 1.0.0
*/
function neliops_add_form( $content ) {
$input = '<input id="neliops_search" type="text" placeholder="Search&hellip;" />';
$results = '<div id="neliops_results"></div>';
return $content . $input . $results;
}//end neliops_add_form
add_filter( 'the_content', 'neliops_add_form' );
<?php
/**
* Enqueues searcher scripts.
*
* @since 1.0.0
*/
function neliops_enqueue_scripts() {
$url = untrailingslashit( plugin_dir_url( __FILE__ ) );
wp_enqueue_script(
'neliops-searcher',
$url . '/searcher.js',
array( 'jquery', 'underscore' ),
'1.0.0',
false
);
wp_enqueue_script(
'neliops-functions',
$url . '/functions.js',
array( 'neliops-searcher' ),
'1.0.0',
false
);
}//end neliops_enqueue_scripts();
add_action( 'wp_enqueue_scripts', 'neliops_enqueue_scripts' );
<?php
/**
* This function returns the posts with a title that contains the query string.
*
* @param string $query the search string.
*
* @return array List of posts. Each element in the array is an associative
* array with the ID, title, and permalink of the matching posts.
*
* @since 1.0.0
*/
function neliops_search_posts( $query ) {
$args = array(
'post_title__like' => $query,
'paged' => 1,
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'desc',
'post_type' => array( 'post' ),
);
add_filter( 'posts_where', 'neliops_add_title_filter_to_wp_query', 10, 2 );
$query = new WP_Query( $args );
remove_filter( 'posts_where', 'neliops_add_title_filter_to_wp_query', 10, 2 );
$result = array();
while ( $query->have_posts() ) {
$query->the_post();
array_push( $result, array(
'ID' => get_the_ID(),
'title' => get_the_title(),
'permalink' => get_the_permalink(),
) );
}//end while
wp_reset_postdata();
return $result;
}//end neliops_search_posts()
<?php
/**
* A filter to search posts based on their title.
*
* This function modifies the posts query so that we can search posts based on
* a term that should appear in their titles.
*
* @param string $where The where clause, as it's originally defined.
* @param WP_Query $wp_query The $wp_query object that contains the params used
* to build the where clause.
*
* @return string wpdb's where statement.
*
* @since 1.0.0
*/
function neliops_add_title_filter_to_wp_query( $where, &$wp_query ) {
global $wpdb;
if ( $search_term = $wp_query->get( 'post_title__like' ) ) {
$search_term = $wpdb->esc_like( $search_term );
$search_term = ' \'%' . $search_term . '%\'';
$where .= ' AND ' . $wpdb->posts . '.post_title LIKE ' . $search_term;
}//end if
return $where;
}//end neliops_add_title_filter_to_wp_query()
add_filter( 'posts_where', 'neliops_add_title_filter_to_wp_query', 10, 2 );
<?php
/**
* AJAX callback for searching posts.
*
* Expected parameters in the request:
* * q: the query string.
*
* @since 1.0.0
*/
function neliops_search_posts_ajax_callback() {
$query = false;
if ( isset( $_REQUEST['q'] ) ) { // Input var okay.
$query = sanitize_text_field( wp_unslash( $_REQUEST['q'] ) ); // Input var okay.
}//end if
if ( false === $query ) {
wp_send_json_error( 'Search string can\'t be empty.' );
}//end if
wp_send_json_success( neliops_search_posts( $query ) );
}//end neliops_search_posts_ajax_callback()
add_action( 'wp_ajax_neliops_search_posts', 'neliops_search_posts_ajax_callback' );
add_action( 'wp_ajax_nopriv_neliops_search_posts', 'neliops_search_posts_ajax_callback' );
/**
* Our searcher class.
*/
function Searcher( $input, $results ) {
this.$input = $input;
this.$results = $results;
this.ajax = false;
this.query = '';
this.searchPostsDebounced = _.debounce( this.searchPosts, 200 );
this.$input.on( 'keyup change', _.bind( this.onQueryChanged, this ) );
}
/**
* Callback on input text change.
*/
Searcher.prototype.onQueryChanged = function() {
var value = this.$input.val();
if ( this.query === value && this.query.length ) {
return;
}//end if
if ( this.ajax ) {
this.ajax.abort();
}//end if
if ( "" === value ) {
this.clear();
} else {
this.searching();
}//end if
this.searchPostsDebounced( value );
};
/**
* This function searches the posts.
*/
Searcher.prototype.searchPosts = function( query ) {
if ( this.ajax ) {
this.ajax.abort();
}//end if
this.query = query;
this.ajax = jQuery.ajax({
url: '/wp-admin/admin-ajax.php',
data: {
action: 'neliops_search_posts',
q: query
},
success: _.bind( this.processResult, this )
});
};
/**
* This function processes the AJAX result and updates the view.
*/
Searcher.prototype.processResult = function( result ) {
if ( ! result.success ) {
this.clear();
}//end if
this.draw( result.data );
};
/**
* This function prints the list of posts in the result area.
*/
Searcher.prototype.draw = function( posts ) {
if ( 0 === posts.length ) {
this.$results.html( 'No posts match the search criteria.' );
return;
}//end if
var list = '<ul>';
for ( var i = 0; i < posts.length; ++i ) {
var post = posts[ i ];
list += '<li><a href="' + post.permalink + '">' + post.title + '</a></li>';
}//end for
list += '</ul>';
this.$results.html( list );
};
/**
* This function modifies the results area to notify the user we're currently searching.
*/
Searcher.prototype.searching = function() {
this.$results.html( 'Searching&hellip;' );
};
/**
* This function clears the result area.
*/
Searcher.prototype.clear = function() {
this.$results.empty();
};
(function( $ ) {
var searcher = new Searcher( $( '#neliops_search' ), $( '#neliops_results' ) );
})( jQuery );
<?php
/**
* Class Search_Form_Test
*
* @package Post_Searcher
*/
class Search_Form_Test extends WP_UnitTestCase {
function test_form_should_appear_at_the_end_of_the_content() {
$pid = $this->factory->post->create( array(
'post_title' => 'A Title',
'post_content' => '<p>Some content.</p>'
) );
$post = get_post( $pid );
$content = apply_filters( 'the_content', $post->post_content );
$this->assertContains( 'neliops_search', $content );
$this->assertContains( 'neliops_results', $content );
}//end test_form_should_appear_at_the_end_of_the_content()
}//end class
<?php
/**
* Class Search_Posts_Function_Test
*
* @package Post_Searcher
*/
class Search_Posts_Function_Test extends WP_UnitTestCase {
function test_if_there_are_no_posts_the_function_should_always_return_an_empty_array() {
$result = neliops_search_posts( '' );
$this->assertInternalType( 'array', $result );
$this->assertEquals( 0, count( $result ) );
$result = neliops_search_posts( 'some title' );
$this->assertInternalType( 'array', $result );
$this->assertEquals( 0, count( $result ) );
}//end test_if_there_are_no_posts_the_function_should_always_return_an_empty_array()
function test_if_there_is_one_post_whose_title_contains_the_query_string_the_function_should_return_this_post_in_an_array() {
$p = $this->factory->post->create( array(
'post_title' => 'Some title here',
) );
$result = neliops_search_posts( 'Some title here' );
$this->assertInternalType( 'array', $result );
$this->assertEquals( 1, count( $result ) );
$result = neliops_search_posts( 'title' );
$this->assertInternalType( 'array', $result );
$this->assertEquals( 1, count( $result ) );
}//end test_if_there_are_no_posts_the_function_should_always_return_an_empty_array()
function test_if_there_are_multiple_posts_whose_titles_match_the_search_criteria_the_function_shoould_include_them_all() {
$this->factory->post->create( array(
'post_title' => 'First title here',
) );
$this->factory->post->create( array(
'post_title' => 'Second title here',
) );
$this->factory->post->create( array(
'post_title' => 'Third title here',
) );
$result = neliops_search_posts( 'title here' );
$this->assertEquals( 3, count( $result ) );
}//end test_if_there_are_multiple_posts_whose_titles_match_the_search_criteria_the_function_shoould_include_them_all()
function test_the_function_should_not_return_posts_that_dont_match_the_search_criteria() {
$match = $this->factory->post->create( array(
'post_title' => 'Valid title',
) );
$miss = $this->factory->post->create( array(
'post_title' => 'Something completely different',
) );
$result = neliops_search_posts( 'Valid title' );
$this->assertEquals( 1, count( $result ) );
$found_post = $result[0];
$this->assertEquals( $match, $found_post['ID'] );
$this->assertNotEquals( $miss, $found_post['ID'] );
}//end test_the_function_should_not_return_posts_that_dont_match_the_search_criteria()
}//end class
<?php
/**
* Class Ajax_Test
*
* @package Post_Searcher
*/
class Ajax_Test extends WP_Ajax_UnitTestCase {
function test_if_there_is_no_query_string_the_callback_should_return_a_non_success_json() {
try {
$this->_handleAjax( 'neliops_search_posts' );
$this->fail( 'Expected exception: WPAjaxDieContinueException' );
} catch ( WPAjaxDieContinueException $e ) {
// We expected this, do nothing.
}//end try
$response = json_decode( $this->_last_response, true );
$this->assertFalse( $response['success'] );
}//end test_if_there_is_no_query_string_the_callback_should_return_a_non_success_json()
function test_if_there_is_a_query_string_the_callback_should_return_a_success_json_with_an_array() {
try {
$_POST['q'] = 'Search string';
$this->_handleAjax( 'neliops_search_posts' );
$this->fail( 'Expected exception: WPAjaxDieContinueException' );
} catch ( WPAjaxDieContinueException $e ) {
// We expected this, do nothing.
}//end try
$response = json_decode( $this->_last_response, true );
$this->assertTrue( $response['success'] );
$this->assertInternalType( 'array', $response['data'] );
}//end test_if_there_is_a_query_string_the_callback_should_return_a_success_json_with_an_array()
}//end class
QUnit.module( 'Searcher class', {}, function() {
QUnit.module( 'Method "draw"', {}, function() {
QUnit.module( 'when the results are empty', {}, function() {
QUnit.test( 'should tell the user no posts match the criteria.', function( assert ) {
var $input = jQuery( '<input type="text" />' );
var $result = jQuery( '<div></div>' );
var searcher = new Searcher( $input, $result );
searcher.draw( [] );
assert.equal( 'No posts match the search criteria.', $result.text() );
});
});
QUnit.module( 'when the results are not empty', {}, function() {
QUnit.test( 'should show all returned posts in a list.', function( assert ) {
var $input = jQuery( '<input type="text" />' );
var $result = jQuery( '<div></div>' );
var searcher = new Searcher( $input, $result );
var posts = [
{ ID: 1, title: 'Title 1', permalink: '#' },
{ ID: 2, title: 'Title 2', permalink: '#' },
{ ID: 3, title: 'Title 3', permalink: '#' }
];
searcher.draw( posts );
assert.equal( $result.find( 'ul li' ).length, posts.length );
});
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment