├── .gitignore ├── LICENSE ├── README.md ├── behat.yml ├── composer.json ├── features ├── blog.feature ├── bootstrap │ └── FeatureContext.php ├── login.feature ├── plugins.feature └── post-manager.feature └── src ├── Context ├── Initializer │ └── WordPressContextInitializer.php └── WordPressContext.php └── ServiceContainer └── WordPressExtension.php /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | vendor 3 | *.phar 4 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Walter Dal Mut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WordPress Extension for Behat 3 2 | =============================== 3 | 4 | This is a Behat 3.0 Extension for WordPress plugin and theme development. 5 | You can use it to test your WordPress installation, or just test your plugin/theme without installing them in a normal WordPress installation (i.e. stand-alone). 6 | The Extension allows you to use WordPress functions in your context class (if you extend it from Johnbillion\WordPressExtension\Context\WordPressContext). 7 | 8 | Installation 9 | ------------ 10 | 11 | 1. Add a composer development requirement for your WordPress theme or plugin: 12 | 13 | ```json 14 | { 15 | "require-dev" : { 16 | "johnbillion/wordpress-behat-extension": "~0.1", 17 | "johnpbloch/wordpress": "~4.0.0" 18 | } 19 | } 20 | ``` 21 | 22 | 2. Add the following Behat configuration file: 23 | 24 | ```yml 25 | default: 26 | suites: 27 | default: 28 | contexts: 29 | - Johnbillion\WordPressExtension\Context\WordPressContext 30 | extensions: 31 | Johnbillion\WordPressExtension: 32 | path: '%paths.base/vendor/wordpress' 33 | 34 | Behat\MinkExtension: 35 | base_url: 'http://localhost:8000' 36 | sessions: 37 | default: 38 | goutte: ~ 39 | 40 | ``` 41 | 42 | 3. Install the vendors and initialize behat test suites 43 | 44 | ```bash 45 | composer update 46 | vendor/bin/behat --init 47 | ``` 48 | 49 | 4. Start your development web server and point its document root to the wordpress directory in vendors (without mail function) 50 | 51 | ```bash 52 | php -S localhost:8000 -t vendor/wordpress -d disable_functions=mail 53 | ``` 54 | 55 | 5. Write some Behat features and test them 56 | 57 | ``` 58 | Feature: Manage plugins 59 | In order to manage plugins 60 | As an admin 61 | I need to enable and disable plugins 62 | 63 | Background: 64 | Given I have a vanilla wordpress installation 65 | | name | email | username | password | 66 | | BDD WordPress | your@email.com | admin | test | 67 | And I am logged in as "admin" with password "test" 68 | 69 | Scenario: Enable the dolly plugin 70 | Given there are plugins 71 | | plugin | status | 72 | | hello.php | enabled | 73 | When I go to "/wp-admin/" 74 | Then I should see a "#dolly" element 75 | 76 | Scenario: Disable the dolly plugin 77 | Given there are plugins 78 | | plugin | status | 79 | | hello.php | disabled | 80 | When I go to "/wp-admin/" 81 | Then I should not see a "#dolly" element 82 | 83 | ``` 84 | 85 | 6. Run the tests 86 | 87 | ```bash 88 | vendor/bin/behat 89 | ``` 90 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | autoload: 3 | - %paths.base%/Features/Context 4 | suites: 5 | default: 6 | contexts: 7 | - Johnbillion\WordPressExtension\Context\WordPressContext 8 | extensions: 9 | Johnbillion\WordPressExtension: 10 | path: '%paths.base/vendor/wordpress' 11 | 12 | Behat\MinkExtension: 13 | base_url: 'http://localhost:8000' 14 | sessions: 15 | default: 16 | goutte: ~ 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "johnbillion/wordpress-behat-extension", 3 | "type": "library", 4 | "description": "WordPress extension for Behat 3", 5 | "authors": [ 6 | { 7 | "name": "wdalmut", 8 | "homepage": "https://github.com/wdalmut" 9 | }, 10 | { 11 | "name": "tmf", 12 | "homepage": "https://github.com/tmf" 13 | }, 14 | { 15 | "name": "johnbillion", 16 | "homepage": "https://github.com/johnbillion" 17 | } 18 | ], 19 | "keywords": [ 20 | "behat", 21 | "wordpress", 22 | "story", 23 | "bdd" 24 | ], 25 | "license": "MIT", 26 | "require": { 27 | "behat/behat": "~3.0,>=3.0.4", 28 | "behat/mink-extension": "~2.0@dev", 29 | "symfony/filesystem": "~2.3", 30 | "symfony/finder": "~2.3" 31 | }, 32 | "require-dev": {}, 33 | "autoload": { 34 | "psr-4": { 35 | "Johnbillion\\WordPressExtension\\": "src" 36 | }, 37 | "files": [ "../../phpunit/phpunit/src/Framework/Assert/Functions.php" ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /features/blog.feature: -------------------------------------------------------------------------------- 1 | Feature: You can read blog posts 2 | In order to read blogs 3 | As a user 4 | I need to go to the blog 5 | 6 | Background: 7 | Given I have a vanilla wordpress installation 8 | | name | email | username | password | 9 | | BDD WordPress | walter.dalmut@gmail.com | admin | test | 10 | And there are posts 11 | | post_title | post_content | post_status | post_author | 12 | | Just my article | The content of my article | publish | 1 | 13 | | My draft | This is just a draft | draft | 1 | 14 | 15 | 16 | Scenario: List my blog posts 17 | Given I am on the homepage 18 | Then I should see "Just my article" 19 | And I should see "Hello World" 20 | And I should not see "My draft" 21 | 22 | Scenario: Read a blog post 23 | Given I am on the homepage 24 | When I follow "Just my article" 25 | Then I should see "Just my article" 26 | And I should see "The content of my article" 27 | 28 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | wordpressParams = $wordpressParams; 34 | $this->minkParams = $minkParams; 35 | $this->basePath = $basePath; 36 | } 37 | 38 | /** 39 | * setup the wordpress environment / stack if the context is a wordpress context 40 | * 41 | * @param Context $context 42 | */ 43 | public function initializeContext(Context $context) 44 | { 45 | if (!$context instanceof WordPressContext) { 46 | return; 47 | } 48 | $this->prepareEnvironment(); 49 | $this->installFileFixtures(); 50 | $this->flushDatabase(); 51 | $this->loadStack(); 52 | } 53 | 54 | /** 55 | * prepare environment variables 56 | */ 57 | private function prepareEnvironment() 58 | { 59 | // wordpress uses these superglobal fields everywhere... 60 | $urlComponents = parse_url($this->minkParams['base_url']); 61 | $_SERVER['HTTP_HOST'] = $urlComponents['host'] . (isset($urlComponents['port']) ? ':' . $urlComponents['port'] : ''); 62 | $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; 63 | 64 | 65 | // we don't have a request uri in headless scenarios: 66 | // wordpress will try to "fix" php_self variable based on the request uri, if not present 67 | $PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php'; 68 | } 69 | 70 | /** 71 | * actually load the wordpress stack 72 | */ 73 | private function loadStack() 74 | { 75 | // prevent wordpress from calling home to api.wordpress.org 76 | if (!defined('WP_INSTALLING') || !WP_INSTALLING) { 77 | define('WP_INSTALLING', true); 78 | } 79 | 80 | $finder = new Finder(); 81 | 82 | // load the wordpress "stack" 83 | $finder->files()->in($this->wordpressParams['path'])->depth('== 0')->name('wp-load.php'); 84 | 85 | foreach ($finder as $bootstrapFile) { 86 | require_once $bootstrapFile->getRealpath(); 87 | } 88 | } 89 | 90 | /** 91 | * create a wp-config.php and link plugins / themes 92 | */ 93 | public function installFileFixtures() 94 | { 95 | $finder = new Finder(); 96 | $fs = new Filesystem(); 97 | $finder->files()->in($this->wordpressParams['path'])->depth('== 0')->name('wp-config-sample.php'); 98 | foreach ($finder as $file) { 99 | $configContent = 100 | str_replace(array( 101 | "'DB_NAME', 'database_name_here'", 102 | "'DB_USER', 'username_here'", 103 | "'DB_PASSWORD', 'password_here'" 104 | ), array( 105 | sprintf("'DB_NAME', '%s'", $this->wordpressParams['connection']['db']), 106 | sprintf("'DB_USER', '%s'", $this->wordpressParams['connection']['username']), 107 | sprintf("'DB_PASSWORD', '%s'", $this->wordpressParams['connection']['password']), 108 | ), $file->getContents()); 109 | $fs->dumpFile($file->getPath() . '/wp-config.php', $configContent); 110 | } 111 | 112 | if (isset($this->wordpressParams['symlink']['from']) && isset($this->wordpressParams['symlink']['to'])) { 113 | $from = $this->wordpressParams['symlink']['from']; 114 | 115 | if (substr($from, 0, 1) != '/') { 116 | $from = $this->basePath . DIRECTORY_SEPARATOR . $from; 117 | } 118 | if ($fs->exists($this->wordpressParams['symlink']['from'])) { 119 | $fs->symlink($from, $this->wordpressParams['symlink']['to']); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * flush the database if specified by flush_database parameter 126 | */ 127 | public function flushDatabase() 128 | { 129 | if ($this->wordpressParams['flush_database']) { 130 | $connection = $this->wordpressParams['connection']; 131 | $mysqli = new \Mysqli( 132 | 'localhost', 133 | $connection['username'], 134 | $connection['password'], 135 | $connection['db'] 136 | ); 137 | 138 | $result = $mysqli->multi_query("DROP DATABASE IF EXISTS ${connection['db']}; CREATE DATABASE ${connection['db']};"); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Context/WordPressContext.php: -------------------------------------------------------------------------------- 1 | getHash(); 36 | $row = $hash[0]; 37 | $name = $row["name"]; 38 | $username = $row["username"]; 39 | $email = $row["email"]; 40 | $password = $row["password"]; 41 | } 42 | 43 | $mysqli = new \Mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME); 44 | $value = $mysqli->multi_query(implode("\n", array( 45 | "DROP DATABASE IF EXISTS " . DB_NAME . ";", 46 | "CREATE DATABASE " . DB_NAME . ";", 47 | ))); 48 | assertTrue($value); 49 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 50 | wp_install($name, $username, $email, true, '', $password); 51 | 52 | $wp_rewrite->init(); 53 | $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); 54 | 55 | } 56 | 57 | /** 58 | * Add these users to this wordpress installation 59 | * 60 | * @see wp_insert_user 61 | * 62 | * @Given /^there are users$/ 63 | */ 64 | public function thereAreUsers(TableNode $table) 65 | { 66 | foreach ($table->getHash() as $userData) { 67 | if (!is_int(wp_insert_user($userData))) { 68 | throw new \InvalidArgumentException("Invalid user information schema."); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Add these posts to this wordpress installation 75 | * 76 | * @see wp_insert_post 77 | * 78 | * @Given /^there are posts$/ 79 | */ 80 | public function thereArePosts(TableNode $table) 81 | { 82 | foreach ($table->getHash() as $postData) { 83 | if (!is_int(wp_insert_post($postData))) { 84 | throw new \InvalidArgumentException("Invalid post information schema."); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Activate/Deactivate plugins 91 | * | plugin | status | 92 | * | plugin/name.php | enabled | 93 | * 94 | * @Given /^there are plugins$/ 95 | */ 96 | public function thereArePlugins(TableNode $table) 97 | { 98 | foreach ($table->getHash() as $row) { 99 | if ($row["status"] == "enabled") { 100 | activate_plugin( $row["plugin"] ); 101 | } else { 102 | deactivate_plugins( $row["plugin"] ); 103 | } 104 | } 105 | } 106 | 107 | 108 | /** 109 | * Login into the reserved area of this wordpress 110 | * 111 | * @Given /^I am logged in as "([^"]*)" with password "([^"]*)"$/ 112 | */ 113 | public function login($username, $password) 114 | { 115 | $this->getSession()->reset(); 116 | $this->visit("wp-login.php"); 117 | $currentPage = $this->getSession()->getPage(); 118 | 119 | $currentPage->fillField('user_login', $username); 120 | $currentPage->fillField('user_pass', $password); 121 | $currentPage->findButton('wp-submit')->click(); 122 | 123 | assertTrue($this->getSession()->getPage()->hasContent('Dashboard')); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/ServiceContainer/WordPressExtension.php: -------------------------------------------------------------------------------- 1 | addDefaultsIfNotSet() 54 | ->children() 55 | ->scalarNode('path') 56 | ->defaultValue(__DIR__ . 'vendor') 57 | ->end() 58 | ->arrayNode('symlink') 59 | ->children() 60 | ->scalarNode('from') 61 | ->defaultValue('.') 62 | ->end() 63 | ->scalarNode('to') 64 | ->isRequired() 65 | ->end() 66 | ->end() 67 | ->end() 68 | ->booleanNode('flush_database') 69 | ->defaultValue(true) 70 | ->end() 71 | ->arrayNode('connection') 72 | ->children() 73 | ->scalarNode('db') 74 | ->defaultValue('wordpress') 75 | ->end() 76 | ->scalarNode('username') 77 | ->defaultValue('root') 78 | ->end() 79 | ->scalarNode('password') 80 | ->defaultValue('') 81 | ->end() 82 | ->end() 83 | ->end() 84 | ->end(); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function load(ContainerBuilder $container, array $config) 91 | { 92 | $this->loadContextInitializer($container); 93 | $container->setParameter('wordpress.parameters', $config); 94 | } 95 | 96 | /** 97 | * Register a Context Initializer service for the behat 98 | * 99 | * @param ContainerBuilder $container the service will check for Johnbillion\WordPressExtension\Context\WordPressContext contexts 100 | */ 101 | private function loadContextInitializer(ContainerBuilder $container) 102 | { 103 | $definition = new Definition('Johnbillion\WordPressExtension\Context\Initializer\WordPressContextInitializer', array( 104 | '%wordpress.parameters%', 105 | '%mink.parameters%', 106 | '%paths.base%', 107 | )); 108 | $definition->addTag(ContextExtension::INITIALIZER_TAG, array('priority' => 0)); 109 | $container->setDefinition('behat.wordpress.service.wordpress_context_initializer', $definition); 110 | } 111 | } 112 | --------------------------------------------------------------------------------