diff --git a/.gitignore b/.gitignore
index b741226..04d68e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
src/Behat
-composer.lock
\ No newline at end of file
+composer.lock
+results
diff --git a/README.md b/README.md
index aa0ef1c..11ac1be 100644
--- a/README.md
+++ b/README.md
@@ -1,86 +1,84 @@
-# Behat for Drupal 8 contribute components.
+# Behat for Drupal 8.
-The module provide BDD testing framework to contribute components.
+The module provide BDD testing framework for contribute components.
## How it's works?
-The Behat we all know and love built from a couple of symofony elements. This
-module use gherkin library which read the cucumber files(AKA *.feature file).
+Each project that uses behat as a testing tool have a FeatureContext class that
+contains the step definition or any other additional information.
-After tearing down the feature file to steps definition behat module will search
-for plugins which their ID match the step.
-
-Eventually the behat module is just a layer which sit above the unit test
-framework and trigger PHP commands.
+Each module will need to to implement a FeatureContext plugin that will keep the
+step definitions and other behat integration(i.e beforeScenario or afterScenario
+methods).
## Define a plugin
-Defining a plugin is very easy. All you need to do is to implement Behat step
-definition plugin. For example the `I visit PATH` plugin:
+Behat module implements a FeatureContext plugin:
```php
visit($url);
- }
-
-}
-```
-
-In the feature file it look like this:
-```cucumber
- Given I visit 'user'
-```
-
-## Define a test
-The test definition is quite easy as well. Create a file under the `Tests`
-folder:
-```php
-drupalCreateUser();
- $this
- ->setPlaceholder('@user-name', $account->label())
- ->setPlaceholder('@user-pass', $account->passRaw)
- ->executeScenario('login', 'behat');
+ $this->placeholders = [
+ '@user-name' => $account->label(),
+ '@user-pass' => $account->passRaw,
+ ];
}
-
}
```
-You can see there are arguments passed to the steps definition via `$this->setPlaceholder()`
-This wll use us later on in the cucumber files. The key method is `$this->executeScenario()`
-Which invoke a file named `login.feature` under the behat module. If you need to
-invoke feature file to a theme component you'll need to write
-`$this->executeScenario('collapsed', 'bootstrap', 'theme');`
+This plugin implements a `beforeScenario` method to a create user for testing.
+The `@runTestsInSeparateProcesses` and `@preserveGlobalState disabled`
+annotation needed by the PHPUnit testing framework for fire up a mink browser
+environment.
+
+## Step definitions
+As mentioned above, the FeatureContext plugin is replacing the FeatureContext
+class. That class will keep all the step definition.
+
+The default step definitions defined in a trait. In this way other modules could
+provide more step definition and your FeatureContext could leverage them.
## Cucumber files
-The cucumber files should be located at MODULE/src/Features/*.feature
+By default all the feature files will be located at MODULE/src/Features. In the
+future, you could specify other folder location in the plugin definition.
+
+## Running the tests
+There are two ways to run the tests. One way is using the UI under
+`admin/config/development/behat` and you can check which files you want to run.
+This isn't a good practice since it's not running in batch operation.
+
+The best way is to use drush: `drush bin/behat PROVIDER URL`.
+
+The `PROVIDER` is the ID for the FeatureContext plugin. In our case is `behat`.
+
+The `URL` is the URL of your Drupal 8 installation.
+
+For example: `drush bin/behat behat http://localhost/drupal8`
+
+Running specific features could be done with the feature option:
+`drush bin/behat behat http://localhost/drupal8 --features=login`
-## Patch
-You'll need to patch Drupal core with the latest [patch](https://www.drupal.org/node/2232861)
+This will run only the login.feature file defined by the behat module.
diff --git a/behat.links.menu.yml b/behat.links.menu.yml
new file mode 100644
index 0000000..364dc2d
--- /dev/null
+++ b/behat.links.menu.yml
@@ -0,0 +1,6 @@
+behat.test_form:
+ title: Behat
+ description: 'Run behat features files.'
+ route_name: behat.test_form
+ parent: system.admin_config_development
+ weight: -6
diff --git a/behat.permissions.yml b/behat.permissions.yml
new file mode 100644
index 0000000..e1c224c
--- /dev/null
+++ b/behat.permissions.yml
@@ -0,0 +1,3 @@
+run behat tests:
+ title: 'Run behat tests'
+ restrict access: true
diff --git a/behat.routing.yml b/behat.routing.yml
index 5825365..3090b86 100644
--- a/behat.routing.yml
+++ b/behat.routing.yml
@@ -6,4 +6,20 @@ behat.state_system_page:
options:
_admin_route: TRUE
requirements:
- _permission: 'access devel information'
\ No newline at end of file
+ _permission: 'run behat tests'
+
+behat.test_form:
+ path: '/admin/config/development/behat'
+ defaults:
+ _form: '\Drupal\behat\Form\BehatTestForm'
+ _title: 'Behat'
+ requirements:
+ _permission: 'run behat tests'
+
+behat.result_form:
+ path: '/admin/config/development/behat/results/{test_id}'
+ defaults:
+ _form: 'Drupal\behat\Form\BehatResultsForm'
+ _title: 'Test result'
+ requirements:
+ _permission: 'administer unit tests'
diff --git a/behat.services.yml b/behat.services.yml
index a716192..81c7667 100644
--- a/behat.services.yml
+++ b/behat.services.yml
@@ -1,4 +1,4 @@
services:
- plugin.manager.behat.step:
+ plugin.manager.behat.FeatureContext:
class: Drupal\behat\BehatPluginManager
parent: default_plugin_manager
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 2291ff0..b9c166e 100644
--- a/composer.json
+++ b/composer.json
@@ -1,8 +1,9 @@
{
"require": {
- "behat/gherkin": "~4.3"
+ "behat/gherkin": "~4.3",
+ "symfony/filesystem": "2.6"
},
"config": {
"vendor-dir": "src/Behat"
}
-}
\ No newline at end of file
+}
diff --git a/composer.phar b/composer.phar
new file mode 100755
index 0000000..8ddf47d
Binary files /dev/null and b/composer.phar differ
diff --git a/drush/behat.drush.inc b/drush/behat.drush.inc
new file mode 100644
index 0000000..a9edead
--- /dev/null
+++ b/drush/behat.drush.inc
@@ -0,0 +1,81 @@
+ dt('Run behat scenarios.'),
+ 'examples' => [
+ 'drush bin/behat behat http://localhost/drupal8' => dt('Run all the features files provided by the behat module.'),
+ 'drush bin/behat behat http://localhost/drupal8 --features=login,comment' => dt('Run selected features files provided by the behat module.'),
+ ],
+ 'arguments' => [
+ 'provider' => dt('The provider ID as defined in the plugin annotation.'),
+ 'url' => dt('The URL for the Drupal installation.'),
+ ],
+ 'options' => [
+ 'features' => dt('List of features in the provider.'),
+ ],
+ 'aliases' => ['bin/behat'],
+ ];
+
+ return $items;
+}
+
+/**
+ * Running the tests using drush.
+ *
+ * @param $provider
+ * The name of the provider as defined in the FeaturesContext plugin
+ * definition.
+ *
+ * @throws \Drupal\behat\Exception\BehatException
+ */
+function drush_behat_run_tests($provider, $url) {
+ if (!$provider_info = Behat::getFeatureContexts($provider)) {
+ throw new Exception\BehatException(dt('The provider @provider was not found', ['@provider' => $provider]));
+ }
+
+ if (!$url) {
+ throw new Exception\BehatException(dt('You must provide the url to the current Drupal installation.'));
+ }
+
+ $class = $provider_info['class'];
+
+ $features = $providers = [];
+
+ // Store the feature context and the defining providers.
+ // todo: Consider themes and features files location defined by provider.
+ $providers[$class] = DRUPAL_ROOT . '/' . drupal_get_path('module', $provider) . '/src/Features/';
+
+ $components = BehatDrushHelper::FeaturesForProvider($provider);
+
+ foreach ($components as $component) {
+ $explode = explode('/', $component);
+ $features[$class][] = end($explode);
+ }
+
+ // todo: populate the values.
+ BehatDrushHelper::SetEnvInformation('FEATURES_PROVIDERS', serialize($providers));
+ BehatDrushHelper::SetEnvInformation('FEATURES_RUN', serialize($features));
+ BehatDrushHelper::SetEnvInformation('SIMPLETEST_BASE_URL', $url);
+ $tests_list['phpunit'][] = $class;
+
+ // Run tests.
+ $test_id = Behat::runTests($tests_list, 'drupal');
+
+ BehatDrushHelper::DisplaySearchResults($test_id);
+}
diff --git a/src/Annotation/Step.php b/src/Annotation/FeatureContext.php
similarity index 63%
rename from src/Annotation/Step.php
rename to src/Annotation/FeatureContext.php
index 1d25b38..afcaeaa 100644
--- a/src/Annotation/Step.php
+++ b/src/Annotation/FeatureContext.php
@@ -9,17 +9,17 @@
use Drupal\Component\Annotation\Plugin;
/**
- * Define a step definition annotation.
+ * Define a feature context plugin.
*
* @Annotation
*/
-class Step extends Plugin {
+class FeatureContext extends Plugin {
/**
* @var String
*
- * The step.
+ * The directory.
*/
- public $step;
+ public $directory;
-}
\ No newline at end of file
+}
diff --git a/src/Behat.php b/src/Behat.php
index 49f4261..460426b 100644
--- a/src/Behat.php
+++ b/src/Behat.php
@@ -9,6 +9,28 @@
class Behat {
+ public static function getFeatureContexts($provider = NULL) {
+ $providers = \Drupal::service('plugin.manager.behat.FeatureContext');
+ return $provider ? $providers->getDefinition($provider) : $providers->getDefinitions();
+ }
+
+ /**
+ * Return list of all the feature files of a module.
+ *
+ * @param $name
+ * The name of the component.
+ * @param $type
+ * The type of the component: module or theme. Default to module.
+ * @param $dir
+ * The directory. Default to src/features.
+ *
+ * @return array
+ * Array of features name.
+ */
+ public static function getComponentFeatures($name, $type = 'module', $dir = 'src/Features') {
+ return glob(drupal_get_path($type, $name) . '/' . $dir . '/*.feature');
+ }
+
/**
* Invoking a step.
*
@@ -20,25 +42,10 @@ class Behat {
* @throws BehatStepException
* @return null|array
*/
- public static function Step(BehatTestsAbstract $behat, $step_definition) {
- $steps = \Drupal::service('plugin.manager.behat.step')->getDefinitions();
-
- foreach ($steps as $step) {
- if ($results = self::stepDefinitionMatch($step['id'], $step_definition)) {
- // Get the step instance.
- $object = \Drupal::service('plugin.manager.behat.step')->createInstance($results['step']);
-
- // Reflect the instance.
- $object_reflection = new \ReflectionClass($object);
- $reflection = new \ReflectionClass($object_reflection->getName());
-
- // Invoke the
- $reflection->getMethod('step')->invokeArgs($object, array($behat) + $results['arguments']);
- return TRUE;
- }
- }
+ public static function FeatureContext(BehatTestsAbstract $behat, $step_definition) {
+ $featureContext = \Drupal::service('plugin.manager.behat.FeatureContext')->getDefinitions();
- throw new BehatStepException($step_definition);
+ return $featureContext;
}
/**
@@ -51,7 +58,7 @@ public static function Step(BehatTestsAbstract $behat, $step_definition) {
* @return array|bool
*/
static public function stepDefinitionMatch($step, $step_definition) {
- if (!preg_match('/' . $step . '/', $step_definition, $matches)) {
+ if (!preg_match($step, $step_definition, $matches)) {
return FALSE;
}
@@ -88,7 +95,7 @@ static public function getParser() {
// Allow other module to alter the parser key words.
\Drupal::moduleHandler()->alter('behat_parser_words', $keywords);
- $lexer = new Lexer($keywords);
+ $lexer = new Lexer($keywords);
return new Parser($lexer);
}
@@ -97,22 +104,55 @@ static public function getParser() {
* This is a dummy method for tests of the behat module.
*/
public static function content() {
- $parser = self::getParser();
-
- foreach (glob(drupal_get_path('module', 'behat') . '/src/features/*.feature') as $feature) {
- $test = file_get_contents($feature);
- $scenarios = $parser->parse($test)->getScenarios();
-
- foreach ($scenarios as $scenario) {
- foreach ($scenario->getSteps() as $step) {
-// dpm($step);
- }
- }
- }
-
$element = array(
'#markup' => 'Hello world!',
);
return $element;
}
-}
\ No newline at end of file
+
+ /**
+ * Find the the step definition from the annotation.
+ *
+ * @param $syntax
+ * The annotation of the method.
+ *
+ * @return string
+ * The step definition.
+ */
+ public static function getBehatStepDefinition($syntax) {
+
+ if (!$start = strpos($syntax, '@Given ')) {
+ return;
+ }
+
+ $explode = explode("\n", substr($syntax, $start + strlen('@Given ')));
+ return $explode[0];
+ }
+
+ /**
+ * Run a list of tests. The function simpletest_run_tests() run the tests but
+ * not passing the test id variable through the environment variable.
+ *
+ * Since we need to run only PHPUnit tests we can set the test ID to the
+ * environment variable and run the behat tests.
+ *
+ * @see simpletest_run_tests().
+ */
+ public static function runTests($test_list) {
+ $test_id = db_insert('simpletest_test_id')
+ ->useDefaults(array('test_id'))
+ ->execute();
+
+ if (!empty($test_list['phpunit'])) {
+ putenv('TESTID=' . $test_id);
+ $phpunit_results = simpletest_run_phpunit_tests($test_id, $test_list['phpunit']);
+ simpletest_process_phpunit_results($phpunit_results);
+ }
+
+ // Early return if there are no further tests to run.
+ if (empty($test_list['simpletest'])) {
+ return $test_id;
+ }
+ }
+
+}
diff --git a/src/BehatBase.php b/src/BehatBase.php
deleted file mode 100644
index a673df0..0000000
--- a/src/BehatBase.php
+++ /dev/null
@@ -1,47 +0,0 @@
-Behat = $Behat;
- }
-
- /**
- * Invoke a step.
- *
- * @param $step
- * The step you need to invoke i.e: "I visit 'user'"
- * @param $placeholders
- * Optional. Placeholder for elements from the step definition.
- *
- * @return $this
- * The current object.
- * .
- * @throws BehatFailedStep
- */
- public function executeStep($step, $placeholders = []) {
- try {
- Behat::Step($this->Behat, format_string($step, $placeholders));
- }
- catch (\Exception $e) {
- throw new BehatFailedStep($e->getMessage());
- }
-
- return $this;
- }
-}
\ No newline at end of file
diff --git a/src/BehatDrushHelper.php b/src/BehatDrushHelper.php
new file mode 100644
index 0000000..cefe319
--- /dev/null
+++ b/src/BehatDrushHelper.php
@@ -0,0 +1,124 @@
+ $component,
+ '@provider' => $provider,
+ ];
+ drush_log(dt('@feature was not found in @provider features folders.', $params), 'warning');
+ continue;
+ }
+
+ $component = $temp_conf;
+ }
+
+ return $components;
+ }
+
+ /**
+ * Set environment variables for the tests.
+ *
+ * @param $name
+ * The variable name.
+ * @param $value
+ * The value.
+ */
+ public static function SetEnvInformation($name, $value) {
+ putenv($name . '=' . $value);
+ }
+
+ /**
+ * Display a cool log with colors and indentation.
+ *
+ * @param $text
+ * The log text.
+ * @param $color
+ * The color of the log: blue, green, red or yellow. Default to green.
+ * @param int $indent
+ * Number of indentation for the text.
+ */
+ public static function coolLog($text, $color = 'green', $indent = 0) {
+ $colors = [
+ 'blue' => 94,
+ 'green' => 92,
+ 'red' => 91,
+ 'yellow' => 93,
+ 'white' => 37,
+ ];
+
+ $string = '';
+ $string .= str_repeat(" ", $indent);
+ $string .= "\033[{$colors[$color]}m{$text}\033[0m\n";
+
+ echo $string;
+ }
+
+ /**
+ * Display the search results.
+ *
+ * @param $test_id
+ * The test ID.
+ */
+ public static function DisplaySearchResults($test_id) {
+ $yml_path = drupal_get_path('module', 'behat') . '/results/behat-' . $test_id . '.yml';
+
+ $parser = new Parser();
+ $logs = $parser->parse(file_get_contents($yml_path));
+
+ foreach ($logs as $feature => $steps) {
+ BehatDrushHelper::coolLog($feature);
+
+ foreach ($steps as $step) {
+ if ($step['status'] == 'pass') {
+ BehatDrushHelper::coolLog($step['step'], 'green', 1);
+ }
+ else {
+ $message = format_string('The tests has failed due to: !error',['!error' => $step['step']]);
+ BehatDrushHelper::coolLog($message, 'red', 1);
+ exit(1);
+ }
+ }
+
+ echo "\n";
+ }
+
+ $file = new FileSystem();
+ $file->remove($yml_path);
+ }
+}
diff --git a/src/BehatPluginManager.php b/src/BehatPluginManager.php
index 7285f6a..9e2806b 100644
--- a/src/BehatPluginManager.php
+++ b/src/BehatPluginManager.php
@@ -27,9 +27,9 @@ class BehatPluginManager extends DefaultPluginManager {
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
- parent::__construct('Plugin/Step', $namespaces, $module_handler, NULL, 'Drupal\behat\Annotation\Step');
- $this->alterInfo('behat_step_alter');
- $this->setCacheBackend($cache_backend, 'behat_steps');
+ parent::__construct('Plugin/FeatureContext', $namespaces, $module_handler, NULL, 'Drupal\behat\Annotation\FeatureContext');
+ $this->alterInfo('behat_feature_context_alter');
+ $this->setCacheBackend($cache_backend, 'behat_feature_context');
}
-}
\ No newline at end of file
+}
diff --git a/src/BehatStepAbstract.php b/src/BehatStepAbstract.php
deleted file mode 100644
index 33c02fc..0000000
--- a/src/BehatStepAbstract.php
+++ /dev/null
@@ -1,33 +0,0 @@
- $object_reflection->getName())));
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/BehatTestsAbstract.php b/src/BehatTestsAbstract.php
index 159fde2..20b274c 100644
--- a/src/BehatTestsAbstract.php
+++ b/src/BehatTestsAbstract.php
@@ -5,8 +5,14 @@
*/
namespace Drupal\behat;
+use Behat\Gherkin\Keywords\ArrayKeywords;
+use Behat\Gherkin\Lexer;
use Behat\Gherkin\Node\ScenarioInterface;
+use Drupal\behat\Exception\BehatStepException;
use Drupal\simpletest\BrowserTestBase;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Yaml\Dumper;
+use Symfony\Component\Yaml\Parser;
/**
* Simple login test.
@@ -35,103 +41,109 @@ class BehatTestsAbstract extends BrowserTestBase {
/**
* @var array
*
- * Metadata info.
+ * Holds placeholders for the scenarios.
*/
- protected $metadata = [];
+ protected $placeholders = [];
/**
- * @var array
+ * @var \Symfony\Component\Filesystem\Filesystem
*
- * Holds placeholders for the scenarios.
+ * Symfony file system object.
*/
- protected $placeholders = [];
+ protected $fileSystem;
/**
- * @var
+ * @var String
*
- * Holds the tag of the running tests.
+ * The yml file path.
*/
- protected $tag;
+ protected $ymlPath;
/**
+ * Get the yml file content.
+ *
* @return mixed
+ * The yml content.
*/
- public function getTag() {
- return $this->tag;
+ public function getYmlFileContent() {
+ $parser = new Parser();
+ return $parser->parse(file_get_contents($this->ymlPath));
}
/**
- * @param mixed $tag
+ * Write the content to the file path.
*
- * @return BehatTestsAbstract
+ * @param $content
*/
- public function setTag($tag) {
- $this->tag = $tag;
- return $this;
+ public function writeYmlFile($content) {
+ $dumper = new Dumper();
+ file_put_contents($this->ymlPath, $dumper->dump($content));
}
/**
- * @param $key
- * @param $value
+ * Before each scenario logout the user.
*
- * @return BehatTestsAbstract
- */
- public function setMetadata($key, $value) {
- $this->metadata[$key] = $value;
- return $this;
- }
- /**
- * @return array
+ * @param $scenarioInterface
+ * The scenario object.
*/
- public function getMetadata() {
- return $this->metadata;
- }
+ public function beforeScenario(ScenarioInterface $scenarioInterface = NULL) {
+ // todo: re-think if this is needed.
+ // $this->prepareEnvironment();
+ // $this->installDrupal();
+ // $this->initMink();
- /**
- * @return array
- */
- public function getEdit() {
- return $this->edit;
+ $this->drupalGet('user/logout');
}
/**
- * @param $key
- * @param $value
+ * Get the test ID.
*
- * @return BehatTestsAbstract
+ * @return integer
+ * The test ID.
*/
- public function setEdit($key, $value) {
- $this->edit[$key] = $value;
- return $this;
+ protected function getTestID() {
+ return getenv('TESTID');
}
/**
- * @param null $key
+ * Get the features we need to run for each provider.
+ *
+ * @param $name
+ * The name of the class namespace. i.e:
+ * Drupal\behat\Plugin\FeatureContext\FeatureContextBase
+ *
* @return array
+ * An array of features files we need to run during the tests.
*/
- public function getPlaceholders($key = NULL) {
- return $key ? $this->placeholders[$key] : $this->placeholders;
- }
+ protected function getFeaturesSettings($name = NULL) {
+ $features = unserialize(getenv('FEATURES_RUN'));
- /**
- * @param $key
- * @param $value
- *
- * @return BehatTestsAbstract
- */
- public function setPlaceholder($key, $value) {
- $this->placeholders[$key] = $value;
- return $this;
+ if ($name && !empty($features[$name])) {
+ return $features[$name];
+ }
+
+ return $features;
}
/**
- * Before each scenario logout the user.
+ * Get the path for the FeatureContext plugin path.
*
- * @param $scenarioInterface
- * The scenario object.
+ * @param $name
+ * The name of the class namespace. i.e:
+ * Drupal\behat\Plugin\FeatureContext\FeatureContextBase
+ *
+ * @return Array|String
+ * Array or a single path of FeatureContext plugin and their features files
+ * path.
*/
- public function beforeScenario(ScenarioInterface $scenarioInterface = NULL) {
- $this->drupalGet('user/logout');
+ protected function getProvidersPath($name = NULL) {
+ $providers = unserialize(getenv('FEATURES_PROVIDERS'));
+
+ if ($name && !empty($providers[$name])) {
+ return $providers[$name];
+ }
+
+ return $providers;
}
/**
@@ -140,49 +152,50 @@ public function beforeScenario(ScenarioInterface $scenarioInterface = NULL) {
* @param $scenarioInterface
* The scenario object.
*/
- public function afterScenario(ScenarioInterface $scenarioInterface = NULL) {}
+ public function afterScenario(ScenarioInterface $scenarioInterface = NULL) {
+ // todo: re-think if this needed.
+ // $this->tearDown();
+ }
/**
- * Execute a scenario from a feature file.
+ * Execute a feature file.
*
- * @param $scenario
- * The name of the scenario file.
- * @param $component
- * Name of the module/theme.
- * @param string $type
- * The type of the component: module or theme. Default is module.
+ * @param $path
+ * The path for the feature file.
*
* @throws \Exception
*/
- public function executeScenario($scenario, $component, $type = 'module') {
- // Get the path of the file.
- $path = DRUPAL_ROOT . '/' . drupal_get_path($type, $component) . '/src/Features/' . $scenario . '.feature';
-
- if (!$path) {
+ public function executeFeature($path) {
+ if (!file_exists($path)) {
throw new \Exception('The scenario is missing from the path ' . $path);
}
$test = file_get_contents($path);
- // Initialize Behat module step manager.
- $StepManager = new BehatBase($this);
-
// Get the parser of the gherkin files.
$parser = Behat::getParser();
$scenarios = $parser->parse($test)->getScenarios();
foreach ($scenarios as $scenario) {
- if ($this->getTag() && !in_array($this->getTag(), $scenario->getTags())) {
- // Run tests with specific tags.
- continue;
- }
-
$this->beforeScenario($scenario);
foreach ($scenario->getSteps() as $step) {
+ try {
+ $this->executeStep(format_string($step->getText(), $this->placeholders));
- // Invoke the steps.
- $StepManager->executeStep($step->getText(), $this->getPlaceholders());
+ // Log the step the file.
+ $this->addLine($scenario->getTitle(), [
+ 'step' => $step->getText(),
+ 'status' => 'pass',
+ ]);
+ }
+ catch (\Exception $e) {
+ $this->addLine($scenario->getTitle(), [
+ 'step' => $step->getText() . "
" . $e->getMessage(),
+ 'status' => 'fail',
+ ]);
+ throw new \Exception($e->getMessage());
+ }
}
$this->afterScenario($scenario);
@@ -190,24 +203,76 @@ public function executeScenario($scenario, $component, $type = 'module') {
}
/**
- * Visiting a Drupal page.
+ * Concatenate values to the yml file.
*
- * @param $path
- * The internal path.
+ * @param $key
+ * The identifier of the rows.
+ * @param $value
+ * Value to concatenate.
*/
- public function visit($path) {
- $this->drupalGet($path);
- $this->assertSession()->statusCodeEquals(200);
+ public function addLine($key, $value) {
+ $content = $this->getYmlFileContent();
+ $content[$key][] = $value;
+ $this->writeYmlFile($content);
}
/**
- * Sending the form.
+ * This method will run all the tests in the current request.
+ */
+ public function testRunTests() {
+ $this->fileSystem = new FileSystem();
+
+ $testid = $this->getTestID();
+
+ // Create the folder of the behat in case it doesn't exists. When displaying
+ // the results we will remove the file for the test.
+ $behat_path = drupal_get_path('module', 'behat') . '/results';
+ $this->ymlPath = $behat_path . '/behat-' . $testid . '.yml';
+
+ if (!$this->fileSystem->exists($behat_path)) {
+ $this->fileSystem->mkdir($behat_path);
+ }
+
+ if (!$this->fileSystem->exists($this->ymlPath)) {
+ $this->fileSystem->touch($this->ymlPath);
+ }
+
+ $reflection = new \ReflectionClass($this);
+ $name = $reflection->getName();
+
+ // Get the base path of the features files.
+ $base_path = $this->getProvidersPath($name);
+
+ foreach ($this->getFeaturesSettings($name) as $feature) {
+ $this->executeFeature($base_path . $feature);
+ }
+ }
+
+ /**
+ * Find in the current instance a method which match the step definition.
+ *
+ * @param $step_definition
+ * The step definition.
*
- * @param $element
- * The submit button element.
+ * @throws BehatStepException
*/
- public function sendForm($element) {
- $this->submitForm($this->edit, $element);
+ protected function executeStep($step_definition) {
+ $reflection = new \ReflectionObject($this);
+ foreach ($reflection->getMethods() as $method) {
+
+ if (!$step = Behat::getBehatStepDefinition($method->getDocComment())) {
+ continue;
+ }
+
+ if ($results = Behat::stepDefinitionMatch($step, $step_definition)) {
+ // Reflect the instance.
+ $object_reflection = new \ReflectionClass($this);
+ $reflection = new \ReflectionClass($object_reflection->getName());
+
+ // Invoke the method.
+ $reflection->getMethod($method->getName())->invokeArgs($this, $results['arguments']);
+ }
+ }
}
}
diff --git a/src/Exception/BehatFailedStep.php b/src/Exception/BehatFailedStep.php
index eec3caf..6d2614e 100644
--- a/src/Exception/BehatFailedStep.php
+++ b/src/Exception/BehatFailedStep.php
@@ -5,7 +5,6 @@
*/
namespace Drupal\behat\Exception;
-
class BehatFailedStep extends \Exception {} {
-}
\ No newline at end of file
+}
diff --git a/src/Features/comment_crud.feature b/src/Features/comment_crud.feature
index e69de29..ae80fad 100644
--- a/src/Features/comment_crud.feature
+++ b/src/Features/comment_crud.feature
@@ -0,0 +1,4 @@
+Feature: Comment crud.
+
+ Scenario: Testing the login form.
+ Given I visit "user"
diff --git a/src/Features/login.feature b/src/Features/login.feature
index 7aceaeb..96d111d 100644
--- a/src/Features/login.feature
+++ b/src/Features/login.feature
@@ -1,17 +1,15 @@
Feature: Login testing.
- @login-success
Scenario: Testing the login form.
- Given I visit 'user'
- And I fill in 'Username' with '@user-name'
- And I fill in 'Password' with '@user-pass'
- When I press 'Log in'
- Then I should see '@user-name'
+ Given I visit "user"
+ And I fill in "Username" with "@user-name"
+ And I fill in "Password" with "@user-pass"
+ When I press "Log in"
+ Then I should see "@user-name"
- @login-failed
- Scenario: Testing the user can't login with bas credentials.
- Given I visit 'user'
- And I fill in 'Username' with 'foo'
- And I fill in 'Password' with 'bar'
- When I press 'Log in'
- Then I should see 'Sorry, unrecognized username or password.'
\ No newline at end of file
+ Scenario: Testing the user can"t login with bas credentials.
+ Given I visit "user"
+ And I fill in "Username" with "foo"
+ And I fill in "Password" with "bar"
+ When I press "Log in"
+ Then I should see "Sorry, unrecognized username or password."
diff --git a/src/Features/node_crud.feature b/src/Features/node_crud._feature
similarity index 100%
rename from src/Features/node_crud.feature
rename to src/Features/node_crud._feature
diff --git a/src/Features/taxonomy_terms_crud.feature b/src/Features/taxonomy_terms_crud._feature
similarity index 100%
rename from src/Features/taxonomy_terms_crud.feature
rename to src/Features/taxonomy_terms_crud._feature
diff --git a/src/FeaturesTraits/BasicTrait.php b/src/FeaturesTraits/BasicTrait.php
new file mode 100644
index 0000000..d6b21c0
--- /dev/null
+++ b/src/FeaturesTraits/BasicTrait.php
@@ -0,0 +1,45 @@
+assertSession()->fieldExists($name);
+ $this->edit[$name] = $value;
+ }
+
+ /**
+ * @Given /^I press "([^"]*)"$/
+ */
+ public function iPress($element) {
+ $button = $this->assertSession()->buttonExists($element);
+
+ if ($button->getAttribute('type') == 'submit') {
+ // This is a submit element. Call the submit form method.
+ $this->submitForm($this->edit, $element);
+ }
+ else {
+ // Normal button. Press it.
+ $button->press();
+ }
+ }
+
+ /**
+ * @Given /^I should see "([^"]*)"$/
+ */
+ public function iShouldSee($text) {
+ $this->assertSession()->pageTextContains($text);
+ }
+
+ /**
+ * @Given /^I visit "([^"]*)"$/
+ */
+ public function iVisit($url) {
+ $this->drupalGet($url);
+ }
+
+}
diff --git a/src/Form/BehatResultsForm.php b/src/Form/BehatResultsForm.php
new file mode 100644
index 0000000..e9b6d7d
--- /dev/null
+++ b/src/Form/BehatResultsForm.php
@@ -0,0 +1,263 @@
+t('No test results to display.'), 'error');
+ return new RedirectResponse($this->url('behat.test_form', array(), array('absolute' => TRUE)));
+ }
+
+ // Load all classes and include CSS.
+ $form['#attached']['library'][] = 'simpletest/drupal.simpletest';
+
+ // Add the results form.
+ $form['test_id'] = $test_id;
+ $filter = static::addResultForm($form, $results, $this->getStringTranslation());
+
+ // Actions.
+ $form['#action'] = $this->url('behat.result_form', array('test_id' => 're-run'));
+ $form['action'] = array(
+ '#type' => 'fieldset',
+ '#title' => $this->t('Actions'),
+ '#attributes' => array('class' => array('container-inline')),
+ '#weight' => -11,
+ );
+
+ $form['action']['filter'] = array(
+ '#type' => 'select',
+ '#title' => 'Filter',
+ '#options' => array(
+ 'all' => $this->t('All (@count)', array('@count' => count($filter['pass']) + count($filter['fail']))),
+ 'pass' => $this->t('Pass (@count)', array('@count' => count($filter['pass']))),
+ 'fail' => $this->t('Fail (@count)', array('@count' => count($filter['fail']))),
+ ),
+ );
+ $form['action']['filter']['#default_value'] = ($filter['fail'] ? 'fail' : 'all');
+
+ // Categorized test classes for to be used with selected filter value.
+ $form['action']['filter_pass'] = array(
+ '#type' => 'hidden',
+ '#default_value' => implode(',', $filter['pass']),
+ );
+ $form['action']['filter_fail'] = array(
+ '#type' => 'hidden',
+ '#default_value' => implode(',', $filter['fail']),
+ );
+
+ $form['action']['op'] = array(
+ '#type' => 'submit',
+ '#value' => $this->t('Run tests'),
+ );
+
+ $form['action']['return'] = array(
+ '#type' => 'link',
+ '#title' => $this->t('Return to list'),
+ '#url' => Url::fromRoute('behat.test_form'),
+ );
+
+ if (FALSE) {
+ $fileSystem = new Filesystem();
+ $fileSystem->remove([drupal_get_path('module', 'behat') . '/results/behat-' . $test_id . '.yml']);
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $pass = $form_state->getValue('filter_pass') ? explode(',', $form_state->getValue('filter_pass')) : array();
+ $fail = $form_state->getValue('filter_fail') ? explode(',', $form_state->getValue('filter_fail')) : array();
+
+ if ($form_state->getValue('filter') == 'all') {
+ $classes = array_merge($pass, $fail);
+ }
+ elseif ($form_state->getValue('filter') == 'pass') {
+ $classes = $pass;
+ }
+ else {
+ $classes = $fail;
+ }
+
+ if (!$classes) {
+ $form_state->setRedirect('behat.test_form');
+ return;
+ }
+
+ $form_execute = array();
+ $form_state_execute = new FormState();
+ foreach ($classes as $class) {
+ $form_state_execute->setValue(['tests', $class], $class);
+ }
+
+ // Submit the simpletest test form to rerun the tests.
+ // Under normal circumstances, a form object's submitForm() should never be
+ // called directly, FormBuilder::submitForm() should be called instead.
+ // However, it calls $form_state->setProgrammed(), which disables the Batch API.
+ $simpletest_test_form = new BehatTestForm();
+ $simpletest_test_form->buildForm($form_execute, $form_state_execute);
+ $simpletest_test_form->submitForm($form_execute, $form_state_execute);
+ if ($redirect = $form_state_execute->getRedirect()) {
+ $form_state->setRedirectUrl($redirect);
+ }
+ }
+
+ /**
+ * Adds the result form to a $form.
+ *
+ * This is a static method so that run-tests.sh can use it to generate a
+ * results page completely external to Drupal. This is why the UI strings are
+ * not wrapped in t().
+ *
+ * @param array $form
+ * The form to attach the results to.
+ * @param array $test_results
+ * The simpletest results.
+ *
+ * @return array
+ * A list of tests the passed and failed. The array has two keys, 'pass' and
+ * 'fail'. Each contains a list of test classes.
+ *
+ * @see simpletest_script_open_browser()
+ * @see run-tests.sh
+ */
+ public static function addResultForm(array &$form, array $results) {
+ $id = $form['test_id'];
+
+ // Transform the test results to be grouped by test class.
+ $test_results = array();
+ foreach ($results as $result) {
+ if (!isset($test_results[$result->test_class])) {
+ $test_results[$result->test_class] = array();
+ }
+ $test_results[$result->test_class][] = $result;
+ }
+
+ $image_status_map = static::buildStatusImageMap();
+
+ // Keep track of which test cases passed or failed.
+ $filter = array(
+ 'pass' => array(),
+ 'fail' => array(),
+ );
+
+ // Summary result widget.
+ $form['result'] = array(
+ '#type' => 'fieldset',
+ '#title' => 'Results',
+ // Because this is used in a theme-less situation need to provide a
+ // default.
+ '#attributes' => array(),
+ );
+ $form['result']['summary'] = $summary = array(
+ '#theme' => 'simpletest_result_summary',
+ '#pass' => 0,
+ '#fail' => 0,
+ '#exception' => 0,
+ '#debug' => 0,
+ );
+
+ \Drupal::service('test_discovery')->registerTestNamespaces();
+
+ $yml_path = drupal_get_path('module', 'behat') . '/results/behat-' . $id . '.yml';
+
+ $parser = new Parser();
+ $logs = $parser->parse(file_get_contents($yml_path));
+
+ // Cycle through each test group.
+ $header = array(
+ 'Message',
+ array('colspan' => 2, 'data' => 'Status')
+ );
+ $form['result']['results'] = array();
+ foreach ($logs as $scenario => $assertions) {
+ // Create group details with summary information.
+ $form['result']['results'][$scenario] = array(
+ '#type' => 'details',
+ '#title' => $scenario,
+ '#open' => TRUE,
+ '#description' => 'voo',
+ );
+ $form['result']['results'][$scenario]['summary'] = $summary;
+ $group_summary =& $form['result']['results'][$scenario]['summary'];
+
+ // Create table of assertions for the group.
+ $rows = array();
+ foreach ($assertions as $assertion) {
+ $row = array();
+ // Assertion messages are in code, so we assume they are safe.
+ $row[] = SafeMarkup::set($assertion['step']);
+ $row[] = $image_status_map[$assertion['status']];
+
+ $class = 'simpletest-' . $assertion['status'];
+ if ($assertion->message_group == 'Debug') {
+ $class = 'simpletest-debug';
+ }
+ $rows[] = array('data' => $row, 'class' => array($class));
+
+ $group_summary['#' . $assertion['status']]++;
+ $form['result']['summary']['#' . $assertion['status']]++;
+ }
+ $form['result']['results'][$scenario]['table'] = array(
+ '#type' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ );
+
+ // Set summary information.
+ $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0;
+ $form['result']['results'][$scenario]['#open'] = !$group_summary['#ok'];
+
+ // Store test group (class) as for use in filter.
+ $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $scenario;
+ }
+
+ // Overall summary status.
+ $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0;
+
+ return $filter;
+ }
+
+}
diff --git a/src/Form/BehatTestForm.php b/src/Form/BehatTestForm.php
new file mode 100644
index 0000000..c2edb09
--- /dev/null
+++ b/src/Form/BehatTestForm.php
@@ -0,0 +1,227 @@
+ 'actions'];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Run tests'),
+ '#tableselect' => TRUE,
+ '#button_type' => 'primary',
+ ];
+
+ // Do not needlessly re-execute a full test discovery if the user input
+ // already contains an explicit list of test classes to run.
+ $user_input = $form_state->getUserInput();
+ if (!empty($user_input['tests'])) {
+ return $form;
+ }
+
+ // JavaScript-only table filters.
+ $form['filters'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => ['table-filter', 'js-show'],
+ ],
+ ];
+ $form['filters']['text'] = [
+ '#type' => 'search',
+ '#title' => $this->t('Search'),
+ '#size' => 30,
+ '#placeholder' => $this->t('Enter test nameā¦'),
+ '#attributes' => [
+ 'class' => ['table-filter-text'],
+ 'data-table' => '#simpletest-test-form',
+ 'autocomplete' => 'off',
+ 'title' => $this->t('Enter at least 3 characters of the test name or description to filter by.'),
+ ],
+ ];
+
+ $form['tests'] = [
+ '#type' => 'table',
+ '#id' => 'simpletest-form-table',
+ '#tableselect' => TRUE,
+ '#header' => [
+ ['data' => $this->t('Test'), 'class' => ['simpletest-test-label']],
+ ['data' => $this->t('File'), 'class' => ['simpletest-test-description']],
+ ],
+ '#empty' => $this->t('No tests to display.'),
+ '#attached' => [
+ 'library' => [
+ 'simpletest/drupal.simpletest',
+ ],
+ ],
+ ];
+
+ // Define the images used to expand/collapse the test groups.
+ $image_collapsed = [
+ '#theme' => 'image',
+ '#uri' => 'core/misc/menu-collapsed.png',
+ '#width' => '7',
+ '#height' => '7',
+ '#alt' => $this->t('Expand'),
+ '#title' => $this->t('Expand'),
+ '#suffix' => '(' . $this->t('Expand') . ')',
+ ];
+ $image_extended = [
+ '#theme' => 'image',
+ '#uri' => 'core/misc/menu-expanded.png',
+ '#width' => '7',
+ '#height' => '7',
+ '#alt' => $this->t('Collapse'),
+ '#title' => $this->t('Collapse'),
+ '#suffix' => '(' . $this->t('Collapse') . ')',
+ ];
+ $form['tests']['#attached']['drupalSettings']['simpleTest']['images'] = [
+ drupal_render($image_collapsed),
+ drupal_render($image_extended),
+ ];
+
+ // Generate the list of tests arranged by group.
+ $groups = Behat::getFeatureContexts();
+ foreach ($groups as $provider => $tests) {
+ $this->providers[] = $tests['class'];
+ $form['tests'][$provider] = [
+ '#attributes' => ['class' => ['simpletest-group']],
+ ];
+
+ // Make the class name safe for output on the page by replacing all
+ // non-word/decimal characters with a dash (-).
+ $group_class = 'module-' . strtolower(trim(preg_replace("/[^\w\d]/", "-", $provider)));
+
+ // Override tableselect column with custom selector for this group.
+ // This group-select-all checkbox is injected via JavaScript.
+ $form['tests'][$provider]['select'] = [
+ '#wrapper_attributes' => [
+ 'id' => $group_class,
+ 'class' => ['simpletest-group-select-all'],
+ ],
+ ];
+ $form['tests'][$provider]['title'] = [
+ // Expand/collapse image.
+ '#prefix' => '