From 0ce63e360a5a840cfd1603668435c6061bc64ea8 Mon Sep 17 00:00:00 2001
From: Matthew Grasmick <matthew.grasmick@acquia.com>
Date: Sun, 13 Jul 2014 17:06:37 -0400
Subject: [PATCH] Adding behat.

---
 composer.json                                      |   3 +
 core/modules/behat/behat.info.yml                  |   6 +
 core/modules/behat/behat.install                   |  27 ++
 core/modules/behat/behat.module                    |  19 +
 core/modules/behat/behat.routing.yml               |   7 +
 core/modules/behat/config/install/behat.dist.yml   |  14 +
 .../behat/config/install/behat.settings.yml        |   1 +
 core/modules/behat/includes/behat.inc              | 527 +++++++++++++++++++++
 core/modules/behat/src/Entity/Scenario.php         |  90 ++++
 core/modules/behat/src/Form/SettingsForm.php       |  66 +++
 core/modules/behat/src/ScenarioStorage.php         |  20 +
 .../behat/tests/features/Installation.feature      |   5 +
 .../tests/features/bootstrap/FeatureContext.php    | 197 ++++++++
 .../quickedit/tests/features/Quickedit.feature     |  13 +
 .../modules/toolbar/tests/features/Toolbar.feature |  20 +
 core/scripts/run-behat-tests.sh                    |  22 +
 16 files changed, 1037 insertions(+)
 create mode 100644 core/modules/behat/behat.info.yml
 create mode 100644 core/modules/behat/behat.install
 create mode 100644 core/modules/behat/behat.module
 create mode 100644 core/modules/behat/behat.routing.yml
 create mode 100644 core/modules/behat/config/install/behat.dist.yml
 create mode 100644 core/modules/behat/config/install/behat.settings.yml
 create mode 100644 core/modules/behat/includes/behat.inc
 create mode 100644 core/modules/behat/src/Entity/Scenario.php
 create mode 100644 core/modules/behat/src/Form/SettingsForm.php
 create mode 100644 core/modules/behat/src/ScenarioStorage.php
 create mode 100644 core/modules/behat/tests/features/Installation.feature
 create mode 100644 core/modules/behat/tests/features/bootstrap/FeatureContext.php
 create mode 100644 core/modules/quickedit/tests/features/Quickedit.feature
 create mode 100644 core/modules/toolbar/tests/features/Toolbar.feature
 create mode 100755 core/scripts/run-behat-tests.sh

diff --git a/composer.json b/composer.json
index 8a78ac5..929d280 100644
--- a/composer.json
+++ b/composer.json
@@ -27,6 +27,9 @@
     "phpunit/phpunit-mock-objects": "dev-master#e60bb929c50ae4237aaf680a4f6773f4ee17f0a2",
     "zendframework/zend-feed": "2.2.*"
   },
+  "require-dev": {
+    "drupal/drupal-extension": "v1.0.2"
+  },
   "autoload": {
     "psr-4": {
       "Drupal\\Core\\": "core/lib/Drupal/Core",
diff --git a/core/modules/behat/behat.info.yml b/core/modules/behat/behat.info.yml
new file mode 100644
index 0000000..487cd40
--- /dev/null
+++ b/core/modules/behat/behat.info.yml
@@ -0,0 +1,6 @@
+name: Behat
+type: module
+description: 'Executes Behat features.'
+package: Core
+version: VERSION
+core: 8.x
diff --git a/core/modules/behat/behat.install b/core/modules/behat/behat.install
new file mode 100644
index 0000000..5931a64
--- /dev/null
+++ b/core/modules/behat/behat.install
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Implements hook_install().
+ */
+function behat_install() {
+  global $base_url;
+  $conf = Drupal::config('behat.dist');
+  $conf->set('default.extensions.Behat\MinkExtension\Extension.base_url', $base_url)
+    ->set('default.extensions.Drupal\DrupalExtension\Extension.drupal.drupal_root', DRUPAL_ROOT)
+    ->save();
+
+  $config = \Drupal::service('config.storage');
+  $data = $config->read('behat.dist');
+
+  // Write to file.
+  $dumper = new \Symfony\Component\Yaml\Dumper();
+  $yaml = $dumper->dump($data, PHP_INT_MAX);
+
+  $destination = 'public://behat.yml';
+  $status = file_unmanaged_save_data($yaml, $destination, FILE_EXISTS_RENAME);
+
+  module_load_include('inc', 'behat', 'includes/behat');
+  behat_discover_modules();
+
+  drupal_set_message('behat.yml has been written to the public files directory. Please update it with the correct base_url.');
+}
diff --git a/core/modules/behat/behat.module b/core/modules/behat/behat.module
new file mode 100644
index 0000000..7666e45
--- /dev/null
+++ b/core/modules/behat/behat.module
@@ -0,0 +1,19 @@
+<?php
+/**
+ * @file
+ * Behat module.
+ */
+
+/**
+ * Implements hook_permission().
+ */
+function behat_permission() {
+  $permissions = array(
+    'administer behat' => array(
+      'title' => t('Administer Behat'),
+      'description' => t('Grants all permission for behat.'),
+      'restrict access' => TRUE,
+    ),
+  );
+  return $permissions;
+}
diff --git a/core/modules/behat/behat.routing.yml b/core/modules/behat/behat.routing.yml
new file mode 100644
index 0000000..211ce00
--- /dev/null
+++ b/core/modules/behat/behat.routing.yml
@@ -0,0 +1,7 @@
+behat.admin:
+  path: '/admin/config/development/behat/settings'
+  defaults:
+    _form: '\Drupal\behat\Form\SettingsForm'
+    _title: 'Behat'
+  requirements:
+    _permission: 'administer behat runner'
diff --git a/core/modules/behat/config/install/behat.dist.yml b/core/modules/behat/config/install/behat.dist.yml
new file mode 100644
index 0000000..e8c2119
--- /dev/null
+++ b/core/modules/behat/config/install/behat.dist.yml
@@ -0,0 +1,14 @@
+default:
+  extensions:
+    Behat\MinkExtension\Extension:
+      goutte: ~
+      default_session: goutte
+      javascript_session: selenium2
+      selenium2:
+        wd_host: 'http://127.0.0.1:4444/wd/hub'
+      base_url: ~
+    Drupal\DrupalExtension\Extension:
+      blackbox: ~
+      api_driver: drupal
+      drupal:
+        drupal_root: ~
diff --git a/core/modules/behat/config/install/behat.settings.yml b/core/modules/behat/config/install/behat.settings.yml
new file mode 100644
index 0000000..a9544d0
--- /dev/null
+++ b/core/modules/behat/config/install/behat.settings.yml
@@ -0,0 +1 @@
+format: filesystem_database
diff --git a/core/modules/behat/includes/behat.inc b/core/modules/behat/includes/behat.inc
new file mode 100644
index 0000000..c954b65
--- /dev/null
+++ b/core/modules/behat/includes/behat.inc
@@ -0,0 +1,527 @@
+<?php
+
+use \Drupal\Core\Extension;
+
+/**
+ * Discovers and registers all modules implementing hook_behat_info().
+ *
+ * @return array
+ *   An array of the modules that were discovered.
+ */
+function behat_discover_modules() {
+  $module_handler = \Drupal::moduleHandler();
+  $modules = $module_handler->getModuleList();
+
+  $discovered = array();
+  foreach ($modules as $module) {
+    $registered = behat_register_module($module);
+    if ($registered) {
+      $discovered[] = $module;
+    }
+  }
+
+  return $discovered;
+}
+
+/**
+ * Registers the locations of all scenarios for a given module.
+ *
+ * @param Drupal\Core\Extension\Extension $module
+ *   The module to register.
+ *
+ * @return bool
+ *   TRUE is the module contained features to register.
+ */
+function behat_register_module(Drupal\Core\Extension\Extension $module) {
+  $registered = behat_module_is_registered($module);
+  // If this module has been registered, remove its scenario registrations.
+  if ($registered) {
+    behat_deregister_module_scenarios($module);
+  }
+
+  // Discover all features for the given module.
+  $features = behat_discover_module_features($module);
+
+  foreach ($features as $feature) {
+    // Register all scenarios for each feature.
+    $status = behat_register_feature_scenarios($feature);
+  }
+
+  return (bool) $features;
+}
+
+/**
+ * Discovers all features for a given module.
+ *
+ * @param \Drupal\Core\Extension\Extension $module
+ *   The module for which to discover features.
+ *
+ * @return array
+ *   An associative array of \Behat\Gherkin\Node\FeatureNode objects, keyed by
+ *   filename.
+ */
+function behat_discover_module_features(\Drupal\Core\Extension\Extension $module) {
+  $system_path = behat_module_features_path($module);
+  $feature_files = file_scan_directory($system_path, '/.*\.feature$/', array('recurse' => TRUE));
+  $parser = behat_get_parser();
+
+  $features = array();
+  foreach ($feature_files as $feature_file) {
+    $feature = $parser->parse(file_get_contents($feature_file->uri));
+    $feature->setFile($feature_file->uri);
+    $feature->drupalModule = $module->getName();
+    $features[$feature_file->filename] = $feature;
+  }
+
+  return $features;
+}
+
+/**
+ * Returns the system file path for a given module's Behat features.
+ *
+ * @param Drupal\Core\Extension\Extension $module
+ *   The module for which to find the features system path.
+ *
+ * @return string
+ *   The system path for the module's Behat features directory.
+ */
+function behat_module_features_path(\Drupal\Core\Extension\Extension $module) {
+  $system_path = DRUPAL_ROOT . '/' . $module->getPath() . '/tests/features';
+
+  return $system_path;
+}
+
+/**
+ * Returns the system file path for a given module's bootstrap directory.
+ *
+ * The bootstrap directory must contain a FeatureContext.php class.
+ *
+ * @param Drupal\Core\Extension\Extension $module
+ *   The module for which to find the bootstrap system path.
+ *
+ * @return string
+ *   The system path for the module's Behat bootstrap directory.
+ */
+function behat_module_bootstrap_path($module) {
+  if (file_exists($module->getPath() . '/tests/features/bootstrap/FeatureContext.php')) {
+    $system_path = DRUPAL_ROOT . '/' . $module->getPath() . '/tests/features/bootstrap';
+  }
+  else {
+    $system_path = DRUPAL_ROOT . '/' . drupal_get_path('module', 'behat') . '/tests/features/bootstrap';
+  }
+
+  return $system_path;
+}
+
+/**
+ * Registers all scenarios for a given feature.
+ *
+ * @param \Behat\Gherkin\Node\FeatureNode $feature
+ *   A Behat feature.
+ */
+function behat_register_feature_scenarios(\Behat\Gherkin\Node\FeatureNode $feature) {
+  $feature_location = behat_convert_absolute_to_relative_path($feature->getFile());
+  $scenarios = $feature->getScenarios();
+
+  foreach ($scenarios as $scenario) {
+    $data['title'] = $scenario->getTitle();
+    // @todo Figure out why this isn't getting the feature!
+    $data['feature'] = $scenario->getFeature()->getTitle();
+    $data['location'] = $feature_location . ':' . $scenario->getLine();
+    $data['module'] = $feature->drupalModule;
+    $entity = entity_create('behat_scenario', $data);
+    $entity->save();
+  }
+
+  // @todo return somesthing here, particularly on failure.
+}
+
+/**
+ * Returns a Behat Parser object for parsing Gherkin.
+ *
+ * @return object
+ *   An Behat\Gherkin\Parser object initialized with default English keywords.
+ */
+function behat_get_parser() {
+  $keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
+    'en' => array(
+      'feature'          => 'Feature',
+      'background'       => 'Background',
+      'scenario'         => 'Scenario',
+      'scenario_outline' => 'Scenario Outline|Scenario Template',
+      'examples'         => 'Examples|Scenarios',
+      'given'            => 'Given',
+      'when'             => 'When',
+      'then'             => 'Then',
+      'and'              => 'And',
+      'but'              => 'But',
+    ),
+  ));
+  $lexer  = new Behat\Gherkin\Lexer($keywords);
+  $parser = new Behat\Gherkin\Parser($lexer);
+
+  return $parser;
+}
+
+/**
+ * Converts a path relative to the drupal root into an absolute system path.
+ *
+ * @param string $relative_path
+ *   A file path, relative the the drupal root.
+ *
+ * @return string
+ *   The absolute system path.
+ */
+function behat_convert_relative_to_absolute_path($relative_path) {
+  return DRUPAL_ROOT . '/' . $relative_path;
+}
+
+/**
+ * Converts an absolute system path to a path relative to the drupal root.
+ *
+ * @param string $absolute_path
+ *   An absolute system path.
+ *
+ * @return string
+ *   The relative path.
+ */
+function behat_convert_absolute_to_relative_path($absolute_path) {
+  return str_replace(DRUPAL_ROOT . '/', '', $absolute_path);
+}
+
+/**
+ * Checks whether a given module containing behat scenarios is registered.
+ *
+ * @param Drupal\Core\Extension\Extension $module
+ *   The module to check.
+ *
+ * @return bool
+ *   TRUE if the module has behat scenarios registered with behat.
+ */
+function behat_module_is_registered(\Drupal\Core\Extension\Extension $module) {
+  $query = 'SELECT bsid FROM {behat_scenario}';
+  $query .= ' WHERE module=:module LIMIT 1';
+  $result = db_query($query, array(':module' => $module->getName()));
+  $record = $result->fetchObject();
+
+  return (boolean) $record;
+}
+
+/**
+ * Looks up all registered scenarios for the given module.
+ *
+ * @param Drupal\Core\Extension\Extension $module
+ *   The module for which to get scenarios.
+ *
+ * @return array
+ *   An array of locations for registered scenarios for the given module, keyed
+ *   by entitiy id.
+ */
+function behat_get_module_scenario_registrations(\Drupal\Core\Extension\Extension $module) {
+  $query = 'SELECT bsid, location FROM {behat_scenario}';
+  $query .= ' WHERE module=:module';
+  $result = db_query($query, array(':module' => $module->getName()));
+  $registrations = $result->fetchAllKeyed(0, 1);
+
+  return $registrations;
+}
+
+/**
+ * De-registers a test location.
+ *
+ * @param int $bsid
+ *   The id for this location.
+ */
+function behat_deregister_scenario($bsid) {
+  entity_delete('behat_scenario', $bsid);
+}
+
+/**
+ * De-registers all scenario registrations for a given module.
+ *
+ * @param Drupal\Core\Extension\Extension $module
+ *   The module to deregister.
+ *
+ * @return bool
+ *   FALSE if the given entity type isn't compatible to the CRUD API.
+ */
+function behat_deregister_module_scenarios(\Drupal\Core\Extension\Extension $module) {
+  $scenario_registrations = behat_get_module_scenario_registrations($module);
+  $entity_ids = array_keys($scenario_registrations);
+
+  return entity_delete_multiple('behat_scenario', $entity_ids);
+}
+
+/**
+ * Gets environmental parameters for Behat.
+ *
+ * @return array
+ *   An associateive array of environmental Behat parameters.
+ */
+function behat_get_env_params() {
+  $behat_params = parse_url(getenv('BEHAT_PARAMS'));
+
+  return $behat_params;
+}
+
+/**
+ * Sets an environmental Behat parameter.
+ *
+ * @param string $key
+ *   The parameter key.
+ *
+ * @param string $value
+ *   The parameter value.
+ *
+ * @return bool
+ *   TRUE if parameter was set successfully.
+ */
+function behat_set_env_param($key, $value) {
+  $behat_params = parse_url(getenv('BEHAT_PARAMS'));
+  $behat_params[$key] = $value;
+
+  foreach ($behat_params as $param_key => $param_value) {
+    if ($param_value === '') {
+      unset($behat_params[$param_key]);
+    }
+  }
+  $behat_params_value = drupal_http_build_query($behat_params);
+
+  return putenv("BEHAT_PARAMS=$behat_params_value");
+}
+
+/**
+ * Execute Behat tests.
+ *
+ * @param array $scenarios
+ *   An array of scenario entities.
+ *
+ * @param array $tags
+ *   An tags to run. If empty, all tests will be run.
+ *
+ * @param array $formats
+ *   An array of formats to output. Valid values are:
+ *   - pretty
+ *   - progress
+ *   - html
+ *   - junit
+ *   - failed
+ *   - snippets
+ *
+ * @return string
+ *   The command line output.
+ */
+function behat_execute_tests($scenarios = array(), $tags = array(), $formats = array()) {
+  $results = '';
+  $module_handler = \Drupal::moduleHandler();
+  $modules = $module_handler->getModuleList();
+
+  // @todo $tags is not currently being used because it doesn't work.
+  $tags = behat_tags_argument_build($tags);
+  $formats = behat_format_arguments_build($formats);
+  $config = behat_config_argument_build();
+  $binary_path = DRUPAL_ROOT . '/core/vendor/bin/behat';
+
+  // Load all scenarios if none have been specified.
+  if (!$scenarios) {
+    $scenarios = entity_load_multiple('behat_scenario');
+  }
+
+  // Execute each scenario separately.
+  foreach ($scenarios as $scenario) {
+    $scenario_location = DRUPAL_ROOT . '/' . $scenario->location->value;
+
+    $module = $modules[$scenario->module->value];
+    $features_path = behat_module_features_path($module);
+    $bootstrap_path = behat_module_bootstrap_path($module);
+
+    // @todo Replace with implementation of behat_set_env_params(). We should
+    // not overwrite the entire BEHAT_PARAMS variable, only a specific key.
+    // behat_set_env_param('paths[features]', $features_path);
+    putenv("BEHAT_PARAMS=paths[features]=$features_path&paths[bootstrap]=$bootstrap_path");
+
+    // Construct the command.
+    $command = array(
+      $binary_path,
+      $formats,
+      $scenario_location,
+      $config,
+    );
+
+    $results .= behat_execute_tests_command($command);
+
+    /*
+    if (variable_get('behat_log', 'filesystem_database')) {
+      list($scenario_file_path, $scenario_line) = explode(':', $scenario->location->value);
+      $result_file_location = $out_dests['junit'] . '/TEST-' . basename($scenario_file_path, '.feature') . '.xml';
+
+      // Log results to database.
+      $junit_result = behat_parse_junit($result_file_location);
+      // @todo Complete logging function.
+      //behat_log_scenario_result($junit_result);
+    }
+    */
+  }
+
+  return $results;
+}
+
+/**
+ * Builds the --formats and --out arguments for the behat command.
+ *
+ * @param array $formats
+ *   An array of behat output formats.
+ *
+ * @return string
+ *   A snippet of the behat command containg format flag.
+ */
+function behat_format_arguments_build($formats) {
+  if (!$formats) {
+    $formats = array('progress', 'junit');
+  }
+
+  // Build an array of output destinations for each format.
+  $out_dests = array();
+  foreach ($formats as $format) {
+    // Junit XML is output to file system.
+    if ($format == 'junit') {
+      $dest_dir = 'public://behat';
+      $output_dir = file_prepare_directory($dest_dir, FILE_CREATE_DIRECTORY);
+      $wrapper = file_stream_wrapper_get_instance_by_uri($dest_dir);
+      $output_dest = $wrapper->realpath();
+      $out_dests[$format] = $output_dest;
+    }
+    // Other formats are written to stdout.
+    else {
+      $out_dests[$format] = '';
+    }
+  }
+
+  $formats = '-f ' . implode(',', $formats);
+  $out = "--out='" . implode(',', $out_dests) . ",'";
+
+  return $formats . ' ' . $out;
+}
+
+/**
+ * Builds the --tags argument for the behat command.
+ *
+ * @param array $tags
+ *   An array of behat tags.
+ *
+ * @return string
+ *   The --tags argument. E.g., "--tags=@sometag".
+ *
+ * @todo Refactor this, it doesn't really work!
+ */
+function behat_tags_argument_build($tags) {
+  $tags_string = '';
+  if (count($tags) > 0) {
+    $tags_string = ' --tags "~@';
+    $tags_string .= implode($tags, '&&@');
+    $tags_string .= '"';
+  }
+
+  return $tags_string;
+}
+
+/**
+ * Builds the --config argument for the behat command.
+ *
+ * @return string
+ *   A snippet of behat command containing the configuration flag.
+ */
+function behat_config_argument_build() {
+  $config_file = drupal_realpath('public://behat.yml');
+
+  return '--config  ' . $config_file;
+}
+
+/**
+ * Executes a command via the Symfony Process compontet.
+ *
+ * @param array $command
+ *   An array of strings, to be imploded and run as a command.
+ *
+ * @return string
+ *   The output of the executed command.
+ */
+function behat_execute_tests_command($command) {
+
+  $command = implode(' ', $command);
+  $process = new \Symfony\Component\Process\Process($command);
+  $process->run(function ($type, $buffer) {
+    print $buffer;
+  });
+
+  if (!$process->isSuccessful()) {
+    $output = $process->getErrorOutput();
+  }
+  else {
+    $output = $process->getOutput();
+  }
+
+  return $output;
+}
+
+/**
+ * Parses a Behat feature result in junit format to a PHP array.
+ *
+ * @param string $result_file_location
+ *   The system path of the junit result file.
+ *
+ * @return array
+ *   An array of the parsed junit result xml.
+ */
+function behat_parse_junit($result_file_location) {
+  $xml = simplexml_load_file($result_file_location);
+  $attributes = (array) $xml->testcase->attributes();
+  $result = $attributes['@attributes'];
+
+  return $result;
+}
+
+/**
+ * Saves results from Behat scenario runs to the database.
+ *
+ * @param array $junit_results
+ *   The parsed junit result xml.
+ */
+function behat_log_scenario_result($junit_results) {
+  $scenarios = entity_load_multiple('behat_scenario');
+
+  // We need to key the entities by their title because the junit results are
+  // keyed by title.
+  $scenarios_by_title = array();
+  foreach ($scenarios as $scenario) {
+    $scenarios_by_title[$scenario->title] = $scenario;
+  }
+
+  foreach ($junit_results as $junit_result) {
+    // Select scenario for this junit result.
+    $scenario = $scenarios_by_title[$junit_result->title];
+
+    // Delete log entries for previous runs for this scenario.
+    $num_deleted = db_delete('behat_log')
+      ->condition('bsid', $scenario->bsid)
+      ->execute();
+
+    $values = array(
+      'bsid' => $scenario->bsid,
+      'time' => '',
+      'assertions' => '',
+      'message' => '',
+      'status' => '',
+      'timestamp' => REQUEST_TIME,
+    );
+
+    // Insert new log entry for current run of this Behat scenario.
+    $blid = db_insert('behat_log')
+      ->fields($values)
+      ->execute();
+
+    // Update scenario entity with the id of the new log entry.
+    $scenario->blid = $blid;
+    // @todo save entity with new blid.
+  }
+}
diff --git a/core/modules/behat/src/Entity/Scenario.php b/core/modules/behat/src/Entity/Scenario.php
new file mode 100644
index 0000000..9ea4918
--- /dev/null
+++ b/core/modules/behat/src/Entity/Scenario.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\behat\Entity\Scenario.
+ */
+
+namespace Drupal\behat\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldDefinition;
+use Drupal\Core\Language\Language;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Defines the behat scenario entity class.
+ *
+ * @ContentEntityType(
+ *   id = "behat_scenario",
+ *   label = @Translation("Behat scenario"),
+ *   controllers = {
+ *     "storage" = "Drupal\Core\Entity\ContentEntityDatabaseStorage",
+ *     "access" = "Drupal\Core\EntityAccessController",
+ *     "list_builder" = "Drupal\Core\EntityListBuilder",
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *   },
+ *   admin_permission = "administer behat scenario",
+ *   base_table = "behat_scenario",
+ *   uri_callback = "behat_scenario_uri",
+ *   fieldable = FALSE,
+ *   translatable = TRUE,
+ *   entity_keys = {
+ *     "id" = "bsid",
+ *     "uuid" = "uuid"
+ *   },
+ *   links = {
+ *     "canonical" = "behat.scenario_view",
+ *     "edit-form" = "behat.scenario_edit",
+ *     "admin-form" = "behat.scenario_settings",
+ *   }
+ * )
+ */
+class Scenario extends ContentEntityBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+
+    $fields['bsid'] = FieldDefinition::create('integer')
+      ->setLabel(t('Behat Scenario ID'))
+      ->setDescription(t('The Behat scenario ID.'))
+      ->setReadOnly(TRUE)
+      ->setSetting('unsigned', TRUE);
+
+    $fields['blid'] = FieldDefinition::create('integer')
+      ->setLabel(t('Behat Log ID'))
+      ->setDescription(t('The Behat log id the most recent run for this scenario. NULL if database logging is disabled.'))
+      ->setSetting('unsigned', TRUE);
+
+    $fields['uuid'] = FieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The Behat scenario UUID.'))
+      ->setReadOnly(TRUE);
+
+    $fields['feature'] = FieldDefinition::create('string')
+      ->setLabel(t('Feature'))
+      ->setDescription(t('The feature to which the Behat scenario belongs.'))
+      ->setSetting('default_value', '');
+
+    $fields['title'] = FieldDefinition::create('string')
+      ->setLabel(t('Title'))
+      ->setDescription(t('The title of the Behat scenario.'))
+      ->setSetting('default_value', '');
+
+    $fields['location'] = FieldDefinition::create('string')
+      ->setLabel(t('Location'))
+      ->setDescription(t('The location of the tests, relative to the Drupal base.'))
+      ->setSetting('default_value', '');
+
+    $fields['module'] = FieldDefinition::create('string')
+      ->setLabel(t('Module'))
+      ->setDescription(t('This module to which this scenario belongs.'))
+      ->setSetting('default_value', '');
+
+    return $fields;
+  }
+}
diff --git a/core/modules/behat/src/Form/SettingsForm.php b/core/modules/behat/src/Form/SettingsForm.php
new file mode 100644
index 0000000..e7f0480
--- /dev/null
+++ b/core/modules/behat/src/Form/SettingsForm.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\behat\Form\SettingsForm.
+ */
+
+namespace Drupal\behat\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Config\ConfigFactory;
+
+/**
+ * Configures behat settings for this site.
+ */
+class SettingsForm extends ConfigFormBase {
+
+  /**
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The factory for configuration objects.
+   */
+  public function __construct(ConfigFactory $config_factory) {
+    parent::__construct($config_factory);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'behat_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  function buildForm(array $form, array &$form_state) {
+    $config = $this->config('behat.settings');
+
+    $form['log'] = array(
+      '#type' => 'select',
+      '#title' => t('Log destination'),
+      '#description' => t('Outputting to the Drupal database allows tests results to be viewed via Views.'),
+      '#options' => array(
+        'filesystem_database' => t('Drupal database and file system'),
+        'filesystem' => t('File system'),
+      ),
+      '#default_value' => $config->get('format'),
+      '#required' => TRUE,
+    );
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+
+    $this->config('behat.settings')
+      ->set('format', $form_state['values']['log'])
+      ->save();
+
+    parent::submitForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/behat/src/ScenarioStorage.php b/core/modules/behat/src/ScenarioStorage.php
new file mode 100644
index 0000000..cb2f616
--- /dev/null
+++ b/core/modules/behat/src/ScenarioStorage.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\behat\ScenarioStorage.
+ */
+
+namespace Drupal\behat;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Entity\ContentEntityDatabaseStorage;
+
+/**
+ * Defines a Controller class for taxonomy terms.
+ */
+class ScenarioStorage extends ContentEntityDatabaseStorage {
+
+}
diff --git a/core/modules/behat/tests/features/Installation.feature b/core/modules/behat/tests/features/Installation.feature
new file mode 100644
index 0000000..d5b1ca7
--- /dev/null
+++ b/core/modules/behat/tests/features/Installation.feature
@@ -0,0 +1,5 @@
+Feature: Site Installation
+
+  Scenario: Installation
+    Given I visit "core/install.php"
+    Then I should see "Drupal already installed"
diff --git a/core/modules/behat/tests/features/bootstrap/FeatureContext.php b/core/modules/behat/tests/features/bootstrap/FeatureContext.php
new file mode 100644
index 0000000..c56d190
--- /dev/null
+++ b/core/modules/behat/tests/features/bootstrap/FeatureContext.php
@@ -0,0 +1,197 @@
+<?php
+
+use Drupal\DrupalExtension\Context\DrupalContext,
+  Drupal\DrupalExtension\Event\EntityEvent;
+
+use Behat\Behat\Exception\PendingException;
+
+use Behat\Gherkin\Node\PyStringNode,
+  Behat\Gherkin\Node\TableNode;
+
+class FeatureContext extends DrupalContext {
+  /**
+   *
+   * @When /^(?:|I )click the element with CSS selector "([^"]*)"$/
+   * @When /^(?:|I )click the element with css selector "([^"]*)"$/
+   */
+  public function iClickTheElementWithCssSelector($css_selector) {
+    $element = $this->getSession()->getPage()->find("css", $css_selector);
+    if (empty($element)) {
+      throw new \Exception(sprintf("The page '%s' does not contain the css selector '%s'", $this->getSession()->getCurrentUrl(), $css_selector));
+    }
+    $element->click();
+  }
+
+  /**
+   * @Then /^I should not see the css selector "([^"]*)"$/
+   * @Then /^I should not see the CSS selector "([^"]*)"$/
+   */
+  public function iShouldNotSeeAElementWithCssSelector($css_selector) {
+    $element = $this->getSession()->getPage()->find("css", $css_selector);
+    if (empty($element)) {
+      throw new \Exception(sprintf("The page '%s' contains the css selector '%s'", $this->getSession()->getCurrentUrl(), $css_selector));
+    }
+  }
+
+  /**
+   * Click on the element with the provided xpath query.
+   *
+   * @When /^I click on the element with xpath "([^"]*)"$/
+   */
+  public function iClickOnTheElementWithXPath($xpath) {
+    // Get the mink session.
+    $session = $this->getSession();
+
+    // Runs the actual query and returns the element.
+    $element = $session->getPage()->find(
+      'xpath',
+      $session->getSelectorsHandler()->selectorToXpath('xpath', $xpath)
+    );
+
+    // Errors must not pass silently.
+    if (NULL === $element) {
+      throw new \InvalidArgumentException(sprintf('Could not evaluate XPath: "%s"', $xpath));
+    }
+
+    // OK, let's click on it.
+    $element->click();
+  }
+
+  /**
+   * @Given /^I (?:should |)see the following <links>$/
+   */
+  public function iShouldSeeTheFollowingLinks(TableNode $table) {
+    $page = $this->getSession()->getPage();
+    $table = $table->getHash();
+    foreach ($table as $key => $value) {
+      $link = $table[$key]['links'];
+      $result = $page->findLink($link);
+      if (empty($result)) {
+        throw new \Exception("The link '" . $link . "' was not found");
+      }
+    }
+  }
+
+  /**
+   * @Given /^I should not see the following <links>$/
+   */
+  public function iShouldNotSeeTheFollowingLinks(TableNode $table) {
+    $page = $this->getSession()->getPage();
+    $table = $table->getHash();
+    foreach ($table as $key => $value) {
+      $link = $table[$key]['links'];
+      $result = $page->findLink($link);
+      if (!empty($result)) {
+        throw new \Exception("The link '" . $link . "' was found");
+      }
+    }
+  }
+
+  /**
+   * @Given /^I should not see the following <texts>$/
+   */
+  public function iShouldNotSeeTheFollowingTexts(TableNode $table) {
+    $page = $this->getSession()->getPage();
+    $table = $table->getHash();
+    foreach ($table as $key => $value) {
+      $text = $table[$key]['texts'];
+      if (!$page->hasContent($text) === FALSE) {
+        throw new \Exception("The text '" . $text . "' was found");
+      }
+    }
+  }
+
+  /**
+   * @Given /^I (?:should |)see the following <texts>$/
+   */
+  public function iShouldSeeTheFollowingTexts(TableNode $table) {
+    $page = $this->getSession()->getPage();
+    $messages = array();
+    $failure_detected = FALSE;
+    $table = $table->getHash();
+    foreach ($table as $key => $value) {
+      $text = $table[$key]['texts'];
+      if ($page->hasContent($text) === FALSE) {
+        $messages[] = "FAILED: The text '" . $text . "' was not found";
+        $failure_detected = TRUE;
+      }
+      else {
+        $messages[] = "PASSED: '" . $text . "'";
+      }
+    }
+    if ($failure_detected) {
+      throw new \Exception(implode("\n", $messages));
+    }
+  }
+
+  /**
+   * Performs a soft reload by re-visting the same URL rather than refreshing.
+   */
+  public function softReload() {
+    $path = $this->getSession()->getCurrentUrl();
+    $this->getSession()->visit($path);
+  }
+
+  /**
+   * @Given /^I am viewing the "([^"]*)" theme$/
+   */
+  public function iAmViewingTheTheme($expected_theme) {
+    global $theme;
+    if ($theme !== $expected_theme) {
+      throw new \Exception(sprintf("'%s' is not the active theme. '%s' is active instead.", $expected_theme, $theme));
+    }
+  }
+
+  /**
+   * Returns the most recently created node.
+   *
+   * @return object
+   *   The most recently created node.
+   */
+  public function getLastCreatedNode() {
+    return $this->getLastCreatedEntity("node");
+  }
+
+  /**
+   * Returns the current, relative path.
+   *
+   * Simply using Drupal's current_path() or $_GET['q'] does not work.
+   *
+   * @return string
+   *   The path.
+   */
+  public function getCurrentPath() {
+    $url = $this->getSession()->getCurrentUrl();
+    $parsed_url = parse_url($url);
+    $path = trim($parsed_url['path'], '/');
+
+    return $path;
+  }
+
+  /**
+   * Returns node currently being viewed. Assumes /node/[nid] URL.
+   *
+   * Using path-based loaders, like menu_load_object(), will not work.
+   *
+   * @return object
+   *   The currently viewed node.
+   *
+   * @throws Exception
+   */
+  public function getNodeFromUrl() {
+
+    $path = $this->getCurrentPath();
+    $system_path = drupal_lookup_path('source', $path);
+    if (!$system_path) {
+      $system_path = $path;
+    }
+    $menu_item = menu_get_item($system_path);
+    if ($menu_item['path'] == 'node/%') {
+      $node = node_load($menu_item['original_map'][1]);
+    }
+    else {
+      throw new \Exception(sprintf("Node could not be loaded from URL '%s'", $path));
+    }
+    return $node;
+  }
+}
diff --git a/core/modules/quickedit/tests/features/Quickedit.feature b/core/modules/quickedit/tests/features/Quickedit.feature
new file mode 100644
index 0000000..8106be5
--- /dev/null
+++ b/core/modules/quickedit/tests/features/Quickedit.feature
@@ -0,0 +1,13 @@
+Feature: Quick edit Link
+  Quick edit link displays quick edit link to edit content.
+  
+  @api @javascript
+  Scenario: Quick Edit Link appears in the content when Quick edit link is clicked.
+    Given I am logged in as a user with the "administrator" role
+    And I am viewing an "article" node with the title "Aardvark"
+    When I click the element with css selector "button.toolbar-icon-edit"
+    And I should see "Open Add new comment configuration options" in the "button.trigger" element
+    And I click the element with CSS selector "button.trigger"
+    And I should see the link "Quick edit"
+    And I click "Quick edit"
+    Then I should see an "div.quickedit-editable" element
diff --git a/core/modules/toolbar/tests/features/Toolbar.feature b/core/modules/toolbar/tests/features/Toolbar.feature
new file mode 100644
index 0000000..e6a9878
--- /dev/null
+++ b/core/modules/toolbar/tests/features/Toolbar.feature
@@ -0,0 +1,20 @@
+Feature: Toolbar
+  The toolbar provides site administration operations.
+
+  @api @javascript
+  Scenario: Toolbar Manage menus appear when the Manage tab is clicked
+    Given I am logged in as a user with the "administrator" role
+    When I click "Manage"
+    Then I should see "Content"
+    # Clean up.
+    And I click "Manage"
+
+  @api @javascript
+  Scenario: Toolbar Manage submenus appear when the Content menu item twisty is clicked
+    Given I am logged in as a user with the "administrator" role
+    When I click "Manage"
+    And I click the element with CSS selector ".toolbar-icon.toolbar-icon-toggle-vertical"
+    And I click the element with CSS selector ".toolbar-handle"
+    Then I should see "Comments"
+    # Clean up.
+    And I click "Manage"
diff --git a/core/scripts/run-behat-tests.sh b/core/scripts/run-behat-tests.sh
new file mode 100755
index 0000000..af66018
--- /dev/null
+++ b/core/scripts/run-behat-tests.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * @file
+ * This script runs Drupal Behat tests from command line.
+ */
+
+require_once __DIR__ . '/../vendor/autoload.php';
+require_once __DIR__ . '/../includes/bootstrap.inc';
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+module_load_include('inc', 'behat', 'includes/behat');
+
+// Change directories so that drupal_realpath() in behat_execute_tests() returns
+// the correct path.
+// @todo Refactor so that this hack isn't necessary.
+chdir(DRUPAL_ROOT);
+
+// Discover and execute Behat tests.
+behat_discover_modules();
+$results = behat_execute_tests();
+print $results;
-- 
1.8.0

