diff --git a/elasticpress.php b/elasticpress.php
index 4bf9e3de47..103afcf278 100644
--- a/elasticpress.php
+++ b/elasticpress.php
@@ -116,6 +116,10 @@ function register_indexable_posts() {
new Feature\Autosuggest\Autosuggest()
);
+ Features::factory()->register_feature(
+ new Feature\DidYouMean\DidYouMean()
+ );
+
Features::factory()->register_feature(
new Feature\WooCommerce\WooCommerce()
);
diff --git a/includes/classes/Elasticsearch.php b/includes/classes/Elasticsearch.php
index 1b2e1db7c9..c72130ba62 100644
--- a/includes/classes/Elasticsearch.php
+++ b/includes/classes/Elasticsearch.php
@@ -468,6 +468,7 @@ public function query( $index, $type, $query, $query_args, $query_object = null
'found_documents' => $total_hits,
'documents' => $documents,
'aggregations' => $response['aggregations'] ?? [],
+ 'suggest' => $response['suggest'] ?? [],
],
$response,
$query,
diff --git a/includes/classes/Feature/DidYouMean/DidYouMean.php b/includes/classes/Feature/DidYouMean/DidYouMean.php
new file mode 100644
index 0000000000..4d5e6d8aae
--- /dev/null
+++ b/includes/classes/Feature/DidYouMean/DidYouMean.php
@@ -0,0 +1,406 @@
+slug = 'did-you-mean';
+
+ $this->title = esc_html__( 'Did You Mean', 'elasticpress' );
+
+ $this->summary = __( 'Recommend alternative search terms for misspelled queries or terms with no results.', 'elasticpress' );
+
+ $this->requires_install_reindex = true;
+
+ $this->available_during_installation = true;
+
+ $this->default_settings = [
+ 'search_behavior' => false,
+ ];
+
+ parent::__construct();
+ }
+
+ /**
+ * Setup search functionality.
+ *
+ * @return void
+ */
+ public function setup() {
+ add_filter( 'ep_post_mapping', [ $this, 'add_mapping' ] );
+ add_filter( 'ep_post_formatted_args', [ $this, 'add_query_args' ], 10, 3 );
+ add_filter( 'ep_integrate_search_queries', [ $this, 'set_ep_suggestion' ], 10, 2 );
+ add_action( 'template_redirect', [ $this, 'automatically_redirect_user' ] );
+ add_action( 'ep_suggestions', [ $this, 'the_output' ] );
+ }
+
+ /**
+ * Output feature box long.
+ *
+ * @return void
+ */
+ public function output_feature_box_long() {
+ ?>
+
+ 'shingle',
+ 'min_shingle_size' => 2,
+ 'max_shingle_size' => 3,
+ ];
+
+ // Custom analyzer.
+ $mapping['settings']['analysis']['analyzer']['trigram'] = [
+ 'type' => 'custom',
+ 'tokenizer' => 'standard',
+ 'filter' => [
+ 'lowercase',
+ 'shingle_filter',
+ ],
+ ];
+
+ if ( version_compare( Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
+ $mapping['mappings']['post']['properties']['post_content']['fields'] = [
+ 'shingle' => [
+ 'type' => 'text',
+ 'analyzer' => 'trigram',
+ ],
+ ];
+ } else {
+ $mapping['mappings']['properties']['post_content']['fields'] = [
+ 'shingle' => [
+ 'type' => 'text',
+ 'analyzer' => 'trigram',
+ ],
+ ];
+ }
+
+ return $mapping;
+ }
+
+ /**
+ * Return the suggested search term.
+ *
+ * @param WP_Query $query WP_Query object
+ * @return string|false
+ */
+ public function get_suggestion( $query = null ) {
+ global $wp_query;
+
+ $settings = $this->get_settings();
+ if ( empty( $settings['active'] ) ) {
+ return false;
+ }
+
+ if ( ! $query && $wp_query->is_main_query() && $wp_query->is_search() ) {
+ $query = $wp_query;
+ }
+
+ if ( ! is_a( $query, '\WP_Query' ) ) {
+ return false;
+ }
+
+ $term = $this->get_suggested_term( $query );
+ if ( empty( $term ) ) {
+ return false;
+ }
+
+ $html = sprintf( '%s: %s?', esc_html__( 'Did you mean', 'elasticpress' ), get_search_link( $term ), $term );
+
+ $html .= $this->get_alternatives_terms( $query );
+ $terms = $query->suggested_terms['options'] ?? [];
+
+ /**
+ * Filter the did you mean suggested HTML.
+ *
+ * @since 4.6.0
+ * @hook ep_suggestion_html
+ * @param {string} $html The HTML output.
+ * @param {array} $terms All suggested terms.
+ * @param {WP_Query} $query The WP_Query object.
+ * @return {string} New HTML output
+ */
+ return apply_filters( 'ep_suggestion_html', $html, $terms, $query );
+ }
+
+ /**
+ * If needed set the `suggest` to ES query clause.
+ *
+ * @param array $formatted_args Formatted Elasticsearch query.
+ * @param array $args WP_Query arguments
+ * @param array $wp_query WP_Query object
+ */
+ public function add_query_args( $formatted_args, $args, $wp_query ) : array {
+ $search_analyzer = [
+ 'phrase' => [
+ 'field' => 'post_content.shingle',
+ 'max_errors' => 2,
+ 'direct_generator' => [
+ [
+ 'field' => 'post_content.shingle',
+ ],
+ ],
+ ],
+ ];
+
+ /**
+ * Filter the search analyzer use for the did you mean feature.
+ *
+ * @since 4.6.0
+ * @hook ep_search_suggestion_analyzer
+ * @param {array} $search_analyzer Search analyzer
+ * @param {array} $formatted_args Formatted Elasticsearch query
+ * @param {array} $args WP_Query arguments
+ * @param {WP_Query} $wp_query WP_Query object
+ * @return {array} New search analyzer
+ */
+ $search_analyzer = apply_filters( 'ep_search_suggestion_analyzer', $search_analyzer, $formatted_args, $args, $wp_query );
+
+ if ( ! empty( $args['s'] ) ) {
+ $formatted_args['suggest'] = array(
+ 'text' => $args['s'],
+ 'ep_suggestion' => $search_analyzer,
+ );
+ }
+
+ return $formatted_args;
+ }
+
+ /**
+ * Set the ep_suggestion flag to true if the query is a search query.
+ *
+ * @param bool $enabled Whether to enable the search queries integration.
+ * @param WP_Query $query The WP_Query object.
+ */
+ public function set_ep_suggestion( $enabled, $query ) : bool {
+ if ( $query->is_search() && ! empty( $query->query_vars['s'] ) ) {
+ $query->set( 'ep_suggestion', true );
+ }
+
+ return $enabled;
+ }
+
+ /**
+ * Returns requirements status of feature
+ *
+ * Requires the search feature to be activated
+ */
+ public function requirements_status() : FeatureRequirementsStatus {
+ $features = Features::factory();
+ $search = $features->get_registered_feature( 'search' );
+
+ if ( ! $search->is_active() ) {
+ return new FeatureRequirementsStatus( 2, esc_html__( 'This feature requires the "Post Search" feature to be enabled', 'elasticpress' ) );
+ }
+
+ return new FeatureRequirementsStatus( 1 );
+ }
+
+ /**
+ * Display feature settings.
+ *
+ * @return void
+ */
+ public function output_feature_box_settings() {
+ $settings = $this->get_settings();
+ ?>
+
+ is_main_query() && $wp_query->is_search() ) {
+ $query = $wp_query;
+ }
+
+ if ( ! is_a( $query, '\WP_Query' ) ) {
+ return false;
+ }
+
+ $settings = $this->get_settings();
+
+ // If there are posts, we don't need to show the list of suggestions.
+ if ( 'list' !== $settings['search_behavior'] || $query->found_posts ) {
+ return false;
+ }
+
+ $options = $query->suggested_terms['options'] ?? [];
+ array_shift( $options );
+
+ if ( empty( $options ) ) {
+ return '';
+ }
+
+ $html = '';
+ $html .= esc_html__( 'Other suggestions:', 'elasticpress' );
+ $html .= '
';
+ foreach ( $options as $option ) {
+ $html .= sprintf( '- %s
', get_search_link( $option['text'] ), $option['text'] );
+ }
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ /**
+ * Returns the top suggested term
+ *
+ * @param WP_Query $query WP_Query object
+ * @return string|bool
+ */
+ public function get_suggested_term( $query ) {
+ $options = $query->suggested_terms['options'] ?? [];
+ return ! empty( $options ) ? $options[0]['text'] : false;
+ }
+
+ /**
+ * Redirect user to suggested search term if no results found and search_behavior is set to redirect.
+ *
+ * @return void
+ */
+ public function automatically_redirect_user() {
+ global $wp_query;
+
+ if ( ! $wp_query->is_main_query() || ! $wp_query->is_search() ) {
+ return;
+ }
+
+ if ( $wp_query->found_posts ) {
+ return;
+ }
+
+ $settings = $this->get_settings();
+ if ( 'redirect' !== $settings['search_behavior'] ) {
+ return;
+ }
+
+ $term = $this->get_suggested_term( $wp_query );
+ if ( empty( $term ) ) {
+ return;
+ }
+
+ $url = get_search_link( $term );
+ $url = add_query_arg(
+ [
+ 'ep_suggestion_original_term' => $wp_query->query_vars['s'],
+ ],
+ $url
+ );
+
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ /**
+ * Return a message to the user when the original search term has no results and the user is redirected to the suggested term.
+ *
+ * @param WP_Query $query WP_Query object
+ *
+ * @return string|void
+ */
+ public function get_original_search_term( $query = null ) {
+ global $wp_query;
+
+ $settings = $this->get_settings();
+ if ( empty( $settings['active'] ) ) {
+ return false;
+ }
+
+ if ( ! $query && $wp_query->is_main_query() && $wp_query->is_search() ) {
+ $query = $wp_query;
+ }
+
+ if ( ! is_a( $query, '\WP_Query' ) ) {
+ return;
+ }
+
+ if ( ! isset( $_GET['ep_suggestion_original_term'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ return;
+ }
+
+ $settings = $this->get_settings();
+ if ( 'redirect' !== $settings['search_behavior'] ) {
+ return;
+ }
+
+ $original_term = sanitize_text_field( wp_unslash( $_GET['ep_suggestion_original_term'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ $html = sprintf(
+ '
+ %s%s
+
+ %s%s
+
',
+ esc_html__( 'Showing results for: ', 'elasticpress' ),
+ esc_html( $query->query_vars['s'] ),
+ esc_html__( 'No results for: ', 'elasticpress' ),
+ esc_html( $original_term )
+ );
+
+ /**
+ * Filter the HTML output for the original search term.
+ *
+ * @since 4.6.0
+ * @hook ep_suggestion_original_search_term_html
+ * @param {string} $html HTML output
+ * @param {string} $search_term Suggested search term
+ * @param {string} $original_term Original search term
+ * @param {WP_Query} $query WP_Query object
+ * @return {string} New HTML output
+ */
+ return apply_filters( 'ep_suggestion_original_search_term_html', $html, $query->query_vars['s'], $original_term, $query );
+ }
+
+ /**
+ * Returns the suggestion
+ *
+ * @param WP_Query $query WP_Query object
+ * @return void
+ */
+ public function the_output( $query = null ) {
+ $html = $this->get_original_search_term( $query );
+ $html .= $this->get_suggestion( $query );
+
+ echo wp_kses_post( $html );
+ }
+}
diff --git a/includes/classes/FeatureRequirementsStatus.php b/includes/classes/FeatureRequirementsStatus.php
index 18f08a1902..39c00171bc 100644
--- a/includes/classes/FeatureRequirementsStatus.php
+++ b/includes/classes/FeatureRequirementsStatus.php
@@ -34,7 +34,7 @@ public function __construct( $code, $message = null ) {
* Returns the status of a feature
*
* 0 is no issues
- * 1 is usable but there are warnngs
+ * 1 is usable but there are warnings
* 2 is not usable
*
* @var int
diff --git a/includes/classes/Indexable/Post/QueryIntegration.php b/includes/classes/Indexable/Post/QueryIntegration.php
index ecf9230d7b..99c9d2f300 100644
--- a/includes/classes/Indexable/Post/QueryIntegration.php
+++ b/includes/classes/Indexable/Post/QueryIntegration.php
@@ -371,6 +371,7 @@ public function get_es_posts( $posts, $query ) {
$query->found_posts = $found_documents;
$query->num_posts = $query->found_posts;
$query->max_num_pages = ceil( $found_documents / $query->get( 'posts_per_page' ) );
+ $query->suggested_terms = isset( $ep_query['suggest']['ep_suggestion'] ) && isset( $ep_query['suggest']['ep_suggestion'][0] ) ? $ep_query['suggest']['ep_suggestion'][0] : [];
$query->elasticsearch_success = true;
// Determine how we should format the results from ES based on the fields parameter.
diff --git a/tests/php/features/TestDidYouMean.php b/tests/php/features/TestDidYouMean.php
new file mode 100644
index 0000000000..d1ff3d6d11
--- /dev/null
+++ b/tests/php/features/TestDidYouMean.php
@@ -0,0 +1,394 @@
+suppress_errors();
+
+ $admin_id = $this->factory->user->create( [ 'role' => 'administrator' ] );
+
+ wp_set_current_user( $admin_id );
+
+ ElasticPress\Features::factory()->activate_feature( 'search' );
+ ElasticPress\Features::factory()->activate_feature( 'did-you-mean' );
+
+ ElasticPress\Features::factory()->setup_features();
+
+ ElasticPress\Elasticsearch::factory()->delete_all_indices();
+ ElasticPress\Indexables::factory()->get( 'post' )->put_mapping();
+
+ $this->setup_test_post_type();
+ }
+
+ /**
+ * Test Feature properties.
+ */
+ public function testConstruct() {
+ $instance = new ElasticPress\Feature\DidYouMean\DidYouMean();
+
+ $this->assertEquals( 'did-you-mean', $instance->slug );
+ $this->assertEquals( 'Did You Mean', $instance->title );
+ $this->assertTrue( $instance->requires_install_reindex );
+ $this->assertTrue( $instance->available_during_installation );
+ $this->assertTrue( $instance->is_visible() );
+ $this->assertSame( [ 'search_behavior' => false ], $instance->default_settings );
+ }
+
+ /**
+ * Test Requirements status.
+ */
+ public function testRequirementsStatus() {
+ $instance = new ElasticPress\Feature\DidYouMean\DidYouMean();
+ $status = $instance->requirements_status();
+
+ $this->assertEquals( 1, $status->code );
+ $this->assertEquals( null, $status->message );
+ }
+
+ /**
+ * Test Requirements status when search feature is not active.
+ */
+ public function testRequirementsStatusWhenSearchFeatureIsNotActive() {
+ ElasticPress\Features::factory()->deactivate_feature( 'search' );
+
+ $instance = new ElasticPress\Feature\DidYouMean\DidYouMean();
+ $status = $instance->requirements_status();
+
+ $this->assertEquals( 2, $status->code );
+ $this->assertEquals( 'This feature requires the "Post Search" feature to be enabled', $status->message );
+ }
+
+ /**
+ * Tests that ES returns a suggestion when search term has a typo.
+ */
+ public function testEsSearchSuggestion() {
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 'test', $query->suggested_terms['options'][0]['text'] );
+ }
+
+ /**
+ * Tests that ES returns a suggestion only for search queries.
+ */
+ public function testEsSearchSuggestionOnlyIntegrateWithSearchQuery() {
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 'ep_integrate' => true,
+ ]
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEmpty( $query->suggested_terms );
+ }
+
+ /**
+ * Tests the get_suggestion method.
+ */
+ public function testGetSearchSuggestionMethod() {
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+
+ $expected = sprintf( 'Did you mean: test?', get_search_link( 'test' ) );
+ $this->assertEquals( $expected, ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->get_suggestion( $query ) );
+ }
+
+ /**
+ * Tests that get_suggestion method returns suggestion only for main query.
+ */
+ public function testGetSearchSuggestionMethodReturnsSuggestionForMainQuery() {
+ global $wp_the_query, $wp_query;
+
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $args = [
+ 's' => 'teet',
+ ];
+ $query = new \WP_Query( $args );
+
+ // mock the query as main query
+ $wp_the_query = $query;
+ $wp_query = $query;
+
+ $this->assertTrue( $query->elasticsearch_success );
+
+ $query = $query->query( $args );
+
+ $expected = sprintf( 'Did you mean: test?', get_search_link( 'test' ) );
+ $this->assertEquals( $expected, ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->get_suggestion() );
+ }
+
+ /**
+ * Tests that get_suggestion method returns false if other than WP_Query is passed.
+ */
+ public function testGetSearchSuggestionMethodReturnsFalseIfOtherThanWpQueryIsPassed() {
+ $query = new \stdClass();
+ $this->assertFalse( ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->get_suggestion( $query ) );
+ }
+
+ /**
+ * Tests that get_suggestion method filter `ep_suggestion_html`.
+ */
+ public function testGetSearchSuggestionMethodFilter() {
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $expected_result = 'Did you mean: test filter is working ?';
+ add_filter(
+ 'ep_suggestion_html',
+ function( $html, $terms, $query ) use ( $expected_result ) {
+ $this->assertEquals( 'test', $terms[0]['text'] );
+ $this->assertInstanceOf( '\WP_Query', $query );
+ return $expected_result;
+ },
+ 10,
+ 3
+ );
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+ $this->assertEquals( $expected_result, ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->get_suggestion( $query ) );
+ }
+
+ /**
+ * Test Mapping for ES version 7 and above.
+ */
+ public function testMapping() {
+ add_filter(
+ 'ep_elasticsearch_version',
+ function() {
+ return '7.0';
+ }
+ );
+
+ $mapping = ElasticPress\Indexables::factory()->get( 'post' )->generate_mapping();
+
+ $expected_result = [
+ 'shingle' =>
+ [
+ 'type' => 'text',
+ 'analyzer' => 'trigram',
+ ],
+ ];
+ $this->assertSame( $expected_result, $mapping['mappings']['properties']['post_content']['fields'] );
+ }
+
+ /**
+ * Test Mapping for ES version lower than 7.
+ */
+ public function testMappingForESVersionLowerThanSeven() {
+ add_filter(
+ 'ep_elasticsearch_version',
+ function() {
+ return '5.2.0';
+ }
+ );
+
+ $mapping = ElasticPress\Indexables::factory()->get( 'post' )->generate_mapping();
+
+ $expected_result = [
+ 'shingle' =>
+ [
+ 'type' => 'text',
+ 'analyzer' => 'trigram',
+ ],
+ ];
+ $this->assertSame( $expected_result, $mapping['mappings']['post']['properties']['post_content']['fields'] );
+ }
+
+ /**
+ * Test `ep_search_suggestion_analyzer` filter.
+ */
+ public function testSearchAnalyzerFilter() {
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $search_analyzer = [
+ 'term' => [
+ 'field' => 'post_content',
+ ],
+ ];
+
+ add_filter(
+ 'ep_search_suggestion_analyzer',
+ function() use ( $search_analyzer ) {
+ return $search_analyzer;
+ }
+ );
+
+ add_filter(
+ 'ep_query_request_args',
+ function( $request_args, $path, $index, $type, $query, $query_args, $query_object ) use ( $search_analyzer ) {
+ $this->assertEquals( $search_analyzer, $query['suggest']['ep_suggestion'] );
+ return $request_args;
+ },
+ 10,
+ 7
+ );
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 'Test post', $query->posts[0]->post_content );
+ }
+
+ /**
+ * Test that function returns the original search term when no results are found.
+ */
+ public function testGetOriginalSearchTerm() {
+ global $wp_the_query, $wp_query;
+
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ ElasticPress\Features::factory()->update_feature(
+ 'did-you-mean',
+ [
+ 'active' => true,
+ 'search_behavior' => 'redirect',
+ ]
+ );
+
+ parse_str( 'ep_suggestion_original_term=Original Term', $_GET );
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ // mock the query as main query
+ $wp_the_query = $query;
+ $wp_query = $query;
+
+ $this->assertTrue( $query->elasticsearch_success );
+
+ $html = ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->get_original_search_term();
+ $this->assertStringContainsString( '', $html );
+ $this->assertStringContainsString( '
Showing results for: teet', $html );
+ $this->assertStringContainsString( '
No results for: Original Term', $html );
+ }
+
+ /**
+ * Test `ep_suggestions` action for main query.
+ */
+ public function testEPSuggestionsAction() {
+ global $wp_the_query, $wp_query;
+
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ ElasticPress\Features::factory()->update_feature(
+ 'did-you-mean',
+ [
+ 'active' => true,
+ 'search_behavior' => 'redirect',
+ ]
+ );
+
+ parse_str( 'ep_suggestion_original_term=Original Term', $_GET );
+
+ // mock the query as main query
+ $wp_the_query = $query;
+ $wp_query = $query;
+
+ ob_start();
+ do_action( 'ep_suggestions' );
+ $output = ob_get_clean();
+
+ $expected = sprintf( '
Did you mean: test?', get_search_link( 'test' ) );
+ $this->assertStringContainsString( $expected, $output );
+ $this->assertStringContainsString( '
Showing results for: teet', $output );
+ }
+
+ /**
+ * Test `ep_suggestions` action for other than main query.
+ */
+ public function testEPSuggestionsActionOtherThanMainQuery() {
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ ElasticPress\Features::factory()->update_feature(
+ 'did-you-mean',
+ [
+ 'active' => true,
+ 'search_behavior' => 'redirect',
+ ]
+ );
+
+ parse_str( 'ep_suggestion_original_term=Original Term', $_GET );
+
+ ob_start();
+ do_action( 'ep_suggestions', $query );
+ $output = ob_get_clean();
+
+ $expected = sprintf( '
Did you mean: test?', get_search_link( 'test' ) );
+ $this->assertStringContainsString( $expected, $output );
+ $this->assertStringContainsString( '
Showing results for: teet', $output );
+ }
+}