Gherkin syntax support in Codeception 02

Discovering tables in Codeception Gherkin support beta.

Catch up

Learning of Gherkin support in Codeception v2.2 I’ve jumped on Codeception development version to try this new feature.
I’ve played with some basic functionalities and Gherkin lingo in my previous post but have not really gone that deep.

Data

To test more functionalities and support I’ve added another feature to the acceptance test suite to test the plugin additions to WordPress vanilla search capabilities:

codeception generate:feature Search acceptance

and filld in my acceptance criteria:

Feature: Search
  In order to find the posts I'm looking for
  As a site visitor
  I need to be able to search posts by title, content, excerpt and comments content.

  Scenario: find a post by title
    Given I am on the "/" page
    And the database contains posts:
      | post_title | post_content | post_excerpt | comments                             |
      | Post 1     | Content One  | Excerpt One  | "Comment one one", "Comment one two" |
      | Post 2     | Content Two  | Excerpt Two  | Comment Two                          |
    When I search for "Post 1"
    Then I see "Post 1" in "main"
    But I don't see "Post 2" in "main"

  Scenario: find a post by content
    Given I am on the "/" page
    And the database contains posts:
      | post_title | post_content | post_excerpt | comments                             |
      | Post 1     | Lorem  | Excerpt One  | "Comment one one", "Comment one two" |
      | Post 2     | Content Two  | Excerpt Two  | Comment Two                          |
    When I search for "Lorem"
    Then I see "Post 1" in "main"
    But I don't see "Post 2" in "main"

  Scenario: find a post by excerpt
    Given I am on the "/" page
    And the database contains posts:
      | post_title | post_content | post_excerpt | comments                             |
      | Post 1     | Content One  | Lorem  | "Comment one one", "Comment one two" |
      | Post 2     | Content Two  | Excerpt Two  | Comment Two                          |
    When I search for "Lorem"
    Then I see "Post 1" in "main"
    But I don't see "Post 2" in "main"

  Scenario: find a post by comment content
    Given I am on the "/" page
    And the database contains posts:
      | post_title | post_content | post_excerpt | comments                             |
      | Post 1     | Content One  | Excerpt One  | "Comment one one", "Lorem" |
      | Post 2     | Content Two  | Excerpt Two  | Comment Two                          |
    When I search for "Lorem"
    Then I see "Post 1" in "main"
    But I don't see "Post 2" in "main"

To support the new assertions and predicates I will ask Codeception to generate the required snippets:

codeception gherkin:snippets acceptance

First aberration here, probably due to this Codeception version still being in beta phase, is that snippets for assertions containing a table input (e.g. “And the database contains posts:”) will be malformed.
Codeception almost working generated snippets
Not a drama but snippets code will require some measure of care to implement.

Snippets support

Again relying on wp-browser modules provided functions and methods coding the new snippets support is easy enoug:

<?php
use Behat\Gherkin\Node\TableNode;


/**
 * Inherited Methods
 * @method void wantToTest($text)
 * @method void wantTo($text)
 * @method void execute($callable)
 * @method void expectTo($prediction)
 * @method void expect($prediction)
 * @method void amGoingTo($argumentation)
 * @method void am($role)
 * @method void lookForwardTo($achieveValue)
 * @method void comment($description)
 * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
 *
 * @SuppressWarnings(PHPMD)
 */
class AcceptanceTester extends \Codeception\Actor
{
    use _generated\AcceptanceTesterActions;

    //...

    /**
     * @Given I am on the :path page
     */
    public function iAmOnThePage($path)
    {
        // from WPBrowser
        $this->amOnPage($path);
    }

    /**
     * @Given the database contains posts:
     */
    public function theDatabaseContainsPosts(TableNode $posts)
    {
        $posts->getTable();
        $table = array_values($posts->getTable());
        $dataRows = array_splice($table, 1);
        $headers = reset($table);

        foreach ($dataRows as $dataRow) {
            $postData = array_combine($headers, $dataRow);
            if (isset($postData['comments'])) {
                $comments = $postData['comments'];
                unset($postData['comments']);
            }

            // from WPDb module
            $id = $this->havePostInDatabase($postData);
            if (!empty($comments)) {
                $comments = array_map(function ($comment) {
                    return trim(trim($comment, '"'));
                }, preg_split('/"\\s*,\\s"/', $comments));
                foreach ($comments as $comment) {

                    // from WPDb module
                    $this->haveCommentInDatabase($id, ['comment_content' => $comment]);
                }
            }
        }
    }

    /**
     * @When I search for :searchString
     */
    public function iSearchFor($searchString)
    {
        // from WPBrowser
        $this->amOnPage('/?s=' . str_replace(' ', '+', $searchString));
    }

    /**
     * @Then I see :text in :selector
     */
    public function iSeeIn($text, $selector)
    {

        // from WPBrowser
        $this->see($text, $selector);
    }

    /**
     * @Then I don't see :text in :selector
     */
    public function iDontSeeIn($text, $selector)
    {
        // from WPBrowser
        $this->dontSee($text, $selector);
    }
}

The notable one among these being the “the database contains posts:” support: it will receive a TableNode input argument and use it to make sure the database will contain the specified posts.
So while the generated snippet code might be slightly off in signature implementing the method itself is easy.

Going for the all green

At this point the plugin code is still stuck on a mere WordPress plugin header so the standard WordPress search function will apply yielding 3 successes and one failure:
Comment content search failure
Updating the plugin code to make the tests pass requires some WordPress filtering:

<?php

/**
 * Plugin Name: Codeception Beta 22
 * Plugin URI: http://theAverageDev.com
 * Description: A WordPress plugin to test Codeception beta 2.2 functionality with WPBrowser.
 * Version: 1.0
 * Author: theAverageDev
 * Author URI: http://theAverageDev.com
 * License: GPL 2.0
 */
class cceptbeta22_SearchFilter
{
    protected $whereFrags = [];

    public function filterJoin($join, WP_Query $query)
    {
        if (is_admin() || !$query->is_search()) {
            return $join;
        }
        /** @var \wpdb $wpdb */
        global $wpdb;

        $join .= " JOIN {$wpdb->comments} ON {$wpdb->posts}.ID = {$wpdb->comments}.comment_post_ID";

        return $join;
    }

    public function filterPostsSearch($where, WP_Query $query)
    {
        if (is_admin() || !$query->is_search()) {
            return $where;
        }

        /** @var \wpdb $wpdb */
        global $wpdb;

        $postsTable = $wpdb->posts;
        $commentsTable = $wpdb->comments;
        $searchFields = [$postsTable . '.post_title', $postsTable . '.post_excerpt', $postsTable . '.post_content', $commentsTable . '.comment_content'];

        $searchTerms = explode(' ', $query->get('s', ''));
        error_log('Where before: ' . $where);
        $where = ' AND ((' . implode(') AND (', array_map(function ($searchTerm) use ($searchTerms, $searchFields) {
                return '(' . implode(') OR (', array_map(function ($searchField) use ($searchTerm) {
                    return "{$searchField} LIKE '%{$searchTerm}%'";
                }, $searchFields)) . ')';
            }, $searchTerms)) . '))';

        $password = $query->get('post_password', '');

        return $where . " AND ({$postsTable}.post_password = '{$password}')";
    }
}

$searchFilter = new cceptbeta22_SearchFilter();

add_filter('posts_join_request', [$searchFilter, 'filterJoin'], 999, 2);
add_filter('posts_search', [$searchFilter, 'filterPostsSearch'], 999, 2);

and will yield the expected results.
I would not use this draft code in production as it’s missing many moving parts yet it will cover the feature as required.
All features green

Next

I will do some more fiddling with Codeception 2.2 beta code before moving to an update of the wp-browser modules suite.

I appreciate your input