diff --git a/README.md b/README.md index ea9ce17..86d19d7 100644 --- a/README.md +++ b/README.md @@ -102,46 +102,37 @@ Example: behat.yml default: context: - parameters: - admin_url: /admin/ - login_url: /Security/login - screenshot_path: %behat.paths.features%/screenshots/ + class: SilverStripe\MyModule\Test\Behaviour\FeatureContext extensions: - SilverStripe\BehatExtension\Extension: - # Assumes path relative to vendor/silverstripe/silverstripe-behat - framework_path: ../../../framework/ - ajax_steps: - - go to - - follow - - press - - click - - submit + SilverStripe\BehatExtension\Extension: ~ Behat\MinkExtension\Extension: # Adjust this to your local environment base_url: http://localhost/ - files_path: %behat.paths.features%/files/ default_session: selenium2 javascript_session: selenium2 goutte: ~ selenium2: browser: firefox -Here's an overview of the non-stndard settings we've added. -You'll need to customize at least the `framework_path` and `base_url` setting. +You'll need to customize at least the `base_url` setting to match the URL where +the tested SilverStripe instance is hosted locally. +Overview of settings (all in the `extensions.SilverStripe\BehatExtension\Extension` path): - * `default.context.parameters.screenshot_path`: Used to store screenshot of a last known state -of a failed step. It defaults to whatever is returned by PHP's `sys_get_temp_dir()`. -Screenshot names within that directory consist of feature file filename and line -number that failed. - * `default.extensions.SilverStripe\BehatExtension\Extension.framework_path`: Path to the SilverStripe Framework folder. It supports both absolute and relative (to `behat.yml` file) paths. - * `default.extensions.Behat\MinkExtension\Extension.base_url`: You will probably need to change the base URL that is used during the test process. + * `framework_path`: Path to the SilverStripe Framework folder. It supports both absolute and relative (to `behat.yml` file) paths. + * `extensions.Behat\MinkExtension\Extension.base_url`: You will probably need to change the base URL that is used during the test process. It is used every time you use relative URLs in your feature descriptions. It will also be used by [file to URL mapping](http://doc.silverstripe.org/framework/en/topics/commandline#configuration) in `SilverStripeExtension`. - * `default.extensions.Behat\MinkExtension\Extension.files_path`: Change to support file uploads in your tests. Currently only absolute paths are supported. - * `default.extensions.SilverStripe\BehatExtension\Extension.ajax_steps`: Because SilverStripe uses AJAX requests quite extensively, we had to invent a way + * `extensions.Behat\MinkExtension\Extension.files_path`: Change to support file uploads in your tests. Currently only absolute paths are supported. + * `ajax_steps`: Because SilverStripe uses AJAX requests quite extensively, we had to invent a way to deal with them more efficiently and less verbose than just Optional `ajax_steps` is used to match steps defined there so they can be "caught" by [special AJAX handlers](http://blog.scur.pl/2012/06/ajax-callback-support-behat-mink/) that tweak the delays. You can either use a pipe delimited string or a list of substrings that match step definition. + * `ajax_timeout`: Milliseconds after which an Ajax request is regarded as timed out, + and the script continues with its assertions to avoid a deadlock (Default: 5000). + * `screenshot_path`: Used to store screenshot of a last known state +of a failed step. It defaults to whatever is returned by PHP's `sys_get_temp_dir()`. +Screenshot names within that directory consist of feature file filename and line +number that failed. ### Additional profiles diff --git a/src/SilverStripe/BehatExtension/Context/BasicContext.php b/src/SilverStripe/BehatExtension/Context/BasicContext.php index 7b1d10d..01cd506 100644 --- a/src/SilverStripe/BehatExtension/Context/BasicContext.php +++ b/src/SilverStripe/BehatExtension/Context/BasicContext.php @@ -107,7 +107,7 @@ JS; */ public function handleAjaxBeforeStep(StepEvent $event) { - $ajax_enabled_steps = $this->getMainContext()->getAjaxEnabledSteps(); + $ajax_enabled_steps = $this->getMainContext()->getAjaxSteps(); $ajax_enabled_steps = implode('|', array_filter($ajax_enabled_steps)); if (empty($ajax_enabled_steps) || !preg_match('/(' . $ajax_enabled_steps . ')/i', $event->getStep()->getText())) { @@ -150,7 +150,7 @@ JS; */ public function handleAjaxAfterStep(StepEvent $event) { - $ajax_enabled_steps = $this->getMainContext()->getAjaxEnabledSteps(); + $ajax_enabled_steps = $this->getMainContext()->getAjaxSteps(); $ajax_enabled_steps = implode('|', array_filter($ajax_enabled_steps)); if (empty($ajax_enabled_steps) || !preg_match('/(' . $ajax_enabled_steps . ')/i', $event->getStep()->getText())) { @@ -171,8 +171,10 @@ JS; public function handleAjaxTimeout() { + $timeoutMs = $this->getMainContext()->getAjaxTimeout(); + // Wait for an ajax request to complete, but only for a maximum of 5 seconds to avoid deadlocks - $this->getSession()->wait(5000, + $this->getSession()->wait($timeoutMs, "(typeof window.__ajaxStatus !== 'undefined' ? window.__ajaxStatus() : 'no ajax') !== 'waiting'" ); @@ -205,35 +207,33 @@ JS; $step = $event->getStep(); $screenshot_path = null; - if (isset($this->context['screenshot_path'])) { - $screenshot_path = realpath($this->context['screenshot_path']); - if (!$screenshot_path) { - \Filesystem::makeFolder($this->context['screenshot_path']); - $screenshot_path = realpath($this->context['screenshot_path']); - } - } - if (!$screenshot_path) { - $screenshot_path = realpath(sys_get_temp_dir()); - } + $path = $this->getMainContext()->getScreenshotPath(); + if(!$path) return; // quit silently when path is not set - if (!file_exists($screenshot_path)) { + $path = realpath($path); + if (!$path) { + \Filesystem::makeFolder($this->context['screenshot_path']); + $path = realpath($this->context['screenshot_path']); + } + + if (!file_exists($path)) { file_put_contents('php://stderr', sprintf('"%s" is not valid directory and failed to create it' . PHP_EOL, $this->context['screenshot_path'])); return; } - if (file_exists($screenshot_path) && !is_dir($screenshot_path)) { + if (file_exists($path) && !is_dir($path)) { file_put_contents('php://stderr', sprintf('"%s" is not valid directory' . PHP_EOL, $this->context['screenshot_path'])); return; } - if (file_exists($screenshot_path) && !is_writable($screenshot_path)) { - file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $screenshot_path)); + if (file_exists($path) && !is_writable($path)) { + file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $path)); return; } - $screenshot_path = sprintf('%s/%s_%d.png', $screenshot_path, basename($feature->getFile()), $step->getLine()); + $path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine()); $screenshot = $driver->wdSession->screenshot(); - file_put_contents($screenshot_path, base64_decode($screenshot)); - file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $screenshot_path)); + file_put_contents($path, base64_decode($screenshot)); + file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path)); } /** diff --git a/src/SilverStripe/BehatExtension/Context/Initializer/SilverStripeAwareInitializer.php b/src/SilverStripe/BehatExtension/Context/Initializer/SilverStripeAwareInitializer.php index 37f3aef..800c651 100644 --- a/src/SilverStripe/BehatExtension/Context/Initializer/SilverStripeAwareInitializer.php +++ b/src/SilverStripe/BehatExtension/Context/Initializer/SilverStripeAwareInitializer.php @@ -24,17 +24,41 @@ use SilverStripe\BehatExtension\Context\SilverStripeAwareContextInterface; */ class SilverStripeAwareInitializer implements InitializerInterface { + private $database_name; - private $ajax_steps; + + /** + * @var Array + */ + protected $ajaxSteps; + + /** + * @var Int Timeout in milliseconds + */ + protected $ajaxTimeout; + + /** + * @var String {@link see SilverStripeContext} + */ + protected $adminUrl; + + /** + * @var String {@link see SilverStripeContext} + */ + protected $loginUrl; + + /** + * @var String {@link see SilverStripeContext} + */ + protected $screenshotPath; /** * Initializes initializer. */ - public function __construct($framework_path, $framework_host, $ajax_steps) + public function __construct($framework_path, $framework_host) { $this->bootstrap($framework_path, $framework_host); $this->database_name = $this->initializeTempDb(); - $this->ajax_steps = $ajax_steps; } public function __destruct() @@ -62,7 +86,61 @@ class SilverStripeAwareInitializer implements InitializerInterface public function initialize(ContextInterface $context) { $context->setDatabase($this->database_name); - $context->setAjaxEnabledSteps($this->ajax_steps); + $context->setAjaxSteps($this->ajaxSteps); + $context->setAjaxTimeout($this->ajaxTimeout); + $context->setScreenshotPath($this->screenshotPath); + $context->setAdminUrl($this->adminUrl); + $context->setLoginUrl($this->loginUrl); + } + + public function setAjaxSteps($ajaxSteps) + { + if($ajaxSteps) $this->ajaxSteps = $ajaxSteps; + } + + public function getAjaxSteps() + { + return $this->ajaxSteps; + } + + public function setAjaxTimeout($ajaxTimeout) + { + $this->ajaxTimeout = $ajaxTimeout; + } + + public function getAjaxTimeout() + { + return $this->ajaxTimeout; + } + + public function setAdminUrl($adminUrl) + { + $this->adminUrl = $adminUrl; + } + + public function getAdminUrl() + { + return $this->adminUrl; + } + + public function setLoginUrl($loginUrl) + { + $this->loginUrl = $loginUrl; + } + + public function getLoginUrl() + { + return $this->loginUrl; + } + + public function setScreenshotPath($screenshotPath) + { + $this->screenshotPath = $screenshotPath; + } + + public function getScreenshotPath() + { + return $this->screenshotPath; } protected function bootstrap($framework_path, $framework_host) diff --git a/src/SilverStripe/BehatExtension/Context/LoginContext.php b/src/SilverStripe/BehatExtension/Context/LoginContext.php index f36474b..b9c5de7 100644 --- a/src/SilverStripe/BehatExtension/Context/LoginContext.php +++ b/src/SilverStripe/BehatExtension/Context/LoginContext.php @@ -53,8 +53,9 @@ class LoginContext extends BehatContext */ public function stepIAmLoggedIn() { - $admin_url = $this->getMainContext()->joinUrlParts($this->getMainContext()->getBaseUrl(), $this->context['admin_url']); - $login_url = $this->getMainContext()->joinUrlParts($this->getMainContext()->getBaseUrl(), $this->context['login_url']); + $c = $this->getMainContext(); + $admin_url = $c->joinUrlParts($c->getBaseUrl(), $c->getAdminUrl()); + $login_url = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl()); $this->getSession()->visit($admin_url); @@ -111,7 +112,8 @@ class LoginContext extends BehatContext */ public function stepILogInWith($email, $password) { - $login_url = $this->getMainContext()->joinUrlParts($this->getMainContext()->getBaseUrl(), $this->context['login_url']); + $c = $this->getMainContext(); + $login_url = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl()); $this->getSession()->visit($login_url); diff --git a/src/SilverStripe/BehatExtension/Context/SilverStripeAwareContextInterface.php b/src/SilverStripe/BehatExtension/Context/SilverStripeAwareContextInterface.php index 2490d88..71f8955 100644 --- a/src/SilverStripe/BehatExtension/Context/SilverStripeAwareContextInterface.php +++ b/src/SilverStripe/BehatExtension/Context/SilverStripeAwareContextInterface.php @@ -30,5 +30,5 @@ interface SilverStripeAwareContextInterface * * @param array $ajax_steps Array of step name parts to match */ - public function setAjaxEnabledSteps($ajax_steps); + public function setAjaxSteps($ajax_steps); } diff --git a/src/SilverStripe/BehatExtension/Context/SilverStripeContext.php b/src/SilverStripe/BehatExtension/Context/SilverStripeContext.php index a29bd4a..ffebf2d 100644 --- a/src/SilverStripe/BehatExtension/Context/SilverStripeContext.php +++ b/src/SilverStripe/BehatExtension/Context/SilverStripeContext.php @@ -26,8 +26,37 @@ require_once 'vendor/autoload.php'; */ class SilverStripeContext extends MinkContext implements SilverStripeAwareContextInterface { - private $database_name; - private $ajax_steps; + protected $database_name; + + /** + * @var Array Partial string match for step names + * that are considered to trigger Ajax request in the CMS, + * and hence need special timeout handling. + * @see \SilverStripe\BehatExtension\Context\BasicContext->handleAjaxBeforeStep(). + */ + protected $ajaxSteps; + + /** + * @var Int Timeout in milliseconds, after which the interface assumes + * that an Ajax request has timed out, and continues with assertions. + */ + protected $ajaxTimeout; + + /** + * @var String Relative URL to the SilverStripe admin interface. + */ + protected $adminUrl; + + /** + * @var String Relative URL to the SilverStripe login form. + */ + protected $loginUrl; + + /** + * @var String Relative path to a writeable folder where screenshots can be stored. + * If set to NULL, no screenshots will be stored. + */ + protected $screenshotPath; protected $context; protected $fixtures; @@ -52,17 +81,54 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex $this->database_name = $database_name; } - public function setAjaxEnabledSteps($ajax_steps) + public function setAjaxSteps($ajaxSteps) { - if (empty($ajax_steps)) { - $ajax_steps = array(); - } - $this->ajax_steps = $ajax_steps; + if($ajaxSteps) $this->ajaxSteps = $ajaxSteps; } - public function getAjaxEnabledSteps() + public function getAjaxSteps() { - return $this->ajax_steps; + return $this->ajaxSteps; + } + + public function setAjaxTimeout($ajaxTimeout) + { + $this->ajaxTimeout = $ajaxTimeout; + } + + public function getAjaxTimeout() + { + return $this->ajaxTimeout; + } + + public function setAdminUrl($adminUrl) + { + $this->adminUrl = $adminUrl; + } + + public function getAdminUrl() + { + return $this->adminUrl; + } + + public function setLoginUrl($loginUrl) + { + $this->loginUrl = $loginUrl; + } + + public function getLoginUrl() + { + return $this->loginUrl; + } + + public function setScreenshotPath($screenshotPath) + { + $this->screenshotPath = $screenshotPath; + } + + public function getScreenshotPath() + { + return $this->screenshotPath; } public function getFixture($data_object) diff --git a/src/SilverStripe/BehatExtension/Extension.php b/src/SilverStripe/BehatExtension/Extension.php index fd11aca..a3286c9 100644 --- a/src/SilverStripe/BehatExtension/Extension.php +++ b/src/SilverStripe/BehatExtension/Extension.php @@ -4,9 +4,10 @@ namespace SilverStripe\BehatExtension; use Symfony\Component\Config\FileLocator, Symfony\Component\DependencyInjection\ContainerBuilder, - Symfony\Component\DependencyInjection\Loader\YamlFileLoader; + Symfony\Component\DependencyInjection\Loader\YamlFileLoader, + Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; -use Behat\Behat\Extension\Extension as BaseExtension; +use Behat\Behat\Extension\ExtensionInterface; /* * This file is part of the SilverStripe\BehatExtension @@ -22,7 +23,7 @@ use Behat\Behat\Extension\Extension as BaseExtension; * * @author MichaƂ Ochman */ -class Extension extends BaseExtension +class Extension implements ExtensionInterface { /** * Loads a specific configuration. @@ -50,6 +51,10 @@ class Extension extends BaseExtension } $container->setParameter('behat.silverstripe_extension.framework_path', $config['framework_path']); + $container->setParameter('behat.silverstripe_extension.admin_url', $config['admin_url']); + $container->setParameter('behat.silverstripe_extension.login_url', $config['login_url']); + $container->setParameter('behat.silverstripe_extension.screenshot_path', $config['screenshot_path']); + $container->setParameter('behat.silverstripe_extension.ajax_timeout', $config['ajax_timeout']); if (isset($config['ajax_steps'])) { $container->setParameter('behat.silverstripe_extension.ajax_steps', $config['ajax_steps']); } @@ -66,4 +71,42 @@ class Extension extends BaseExtension new Compiler\MinkExtensionBaseUrlPass(), ); } + + /** + * Setups configuration for current extension. + * + * @param ArrayNodeDefinition $builder + */ + function getConfig(ArrayNodeDefinition $builder) + { + $builder-> + children()-> + scalarNode('framework_path')-> + defaultValue('../../../framework')-> + end()-> + scalarNode('screenshot_path')-> + defaultNull()-> + end()-> + scalarNode('admin_url')-> + defaultValue('/admin/')-> + end()-> + scalarNode('login_url')-> + defaultValue('/Security/login')-> + end()-> + scalarNode('ajax_timeout')-> + defaultValue(5000)-> + end()-> + arrayNode('ajax_steps')-> + defaultValue(array( + 'go to', + 'follow', + 'press', + 'click', + 'submit' + ))-> + prototype('scalar')-> + end()-> + end()-> + end(); + } } diff --git a/src/SilverStripe/BehatExtension/services/silverstripe.yml b/src/SilverStripe/BehatExtension/services/silverstripe.yml index bec348a..b34fae3 100644 --- a/src/SilverStripe/BehatExtension/services/silverstripe.yml +++ b/src/SilverStripe/BehatExtension/services/silverstripe.yml @@ -2,12 +2,21 @@ parameters: behat.silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer behat.silverstripe_extension.framework_path: ~ behat.silverstripe_extension.ajax_steps: ~ + behat.silverstripe_extension.ajax_timeout: ~ + behat.silverstripe_extension.admin_url: ~ + behat.silverstripe_extension.login_url: ~ + behat.silverstripe_extension.screenshot_path: ~ services: behat.silverstripe_extension.context.initializer: class: %behat.silverstripe_extension.context.initializer.class% arguments: - %behat.silverstripe_extension.framework_path% - %behat.silverstripe_extension.framework_host% - - %behat.silverstripe_extension.ajax_steps% + calls: + - [setAjaxSteps, [%behat.silverstripe_extension.ajax_steps%]] + - [setAjaxTimeout, [%behat.silverstripe_extension.ajax_timeout%]] + - [setAdminUrl, [%behat.silverstripe_extension.admin_url%]] + - [setLoginUrl, [%behat.silverstripe_extension.login_url%]] + - [setScreenshotPath, [%behat.silverstripe_extension.screenshot_path%]] tags: - { name: behat.context.initializer }