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..68efcbf --- /dev/null +++ b/core/modules/behat/behat.install @@ -0,0 +1,25 @@ +getModule('behat')->getPath() . '/config/behat.yml.dist'); + $behat_conf['default']['extensions']['Behat\MinkExtension']['base_url'] = $base_url; + $behat_conf['default']['extensions']['Drupal\DrupalExtension']['drupal']['drupal_root'] = $base_url; + + // Write to file. + $yaml = Yaml::dump($behat_conf, 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..f6fc9e8 --- /dev/null +++ b/core/modules/behat/behat.module @@ -0,0 +1,19 @@ + [ + '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/behat.yml.dist b/core/modules/behat/config/behat.yml.dist new file mode 100644 index 0000000..498120c --- /dev/null +++ b/core/modules/behat/config/behat.yml.dist @@ -0,0 +1,30 @@ +default: + suites: + default: + contexts: + - FeatureContext + - Drupal\DrupalExtension\Context\DrupalContext + - Drupal\DrupalExtension\Context\MessageContext + - Drupal\DrupalExtension\Context\MinkContext + - Drupal\DrupalExtension\Context\MarkupContext + extensions: + Behat\MinkExtension: + goutte: ~ + selenium2: ~ + base_url: ~ + Drupal\DrupalExtension: + blackbox: ~ + api_driver: 'drupal' + drupal: + drupal_root: ~ + region_map: + content: "#content" + footer: "#footer" + left header: "#header-left" + right header: "#header-right" + right sidebar: "#aside-region" + selectors: + message_selector: '.messages' + error_message_selector: '.messages.error' + success_message_selector: '.messages.status' + warning_message_selector: '.messages.warning' \ No newline at end of file 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/config/schema/behat.schema.yml b/core/modules/behat/config/schema/behat.schema.yml new file mode 100644 index 0000000..ca71370 --- /dev/null +++ b/core/modules/behat/config/schema/behat.schema.yml @@ -0,0 +1,7 @@ +behat.settings: + type: config_object + label: 'Behat module settings' + mapping: + format: + type: string + label: 'Logging format' diff --git a/core/modules/behat/includes/behat.inc b/core/modules/behat/includes/behat.inc new file mode 100644 index 0000000..17028f1 --- /dev/null +++ b/core/modules/behat/includes/behat.inc @@ -0,0 +1,528 @@ +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(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(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) { + /* @var Behat\Gherkin\Node\FeatureNode $feature */ + $feature = $parser->parse(file_get_contents($feature_file->uri)); + $feature->drupalModule = $module->getName(); + $features[$feature->getFile()] = $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(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(FeatureNode $feature) { + $feature_location = behat_convert_absolute_to_relative_path($feature->getFile()); + $scenarios = $feature->getScenarios(); + + foreach ($scenarios as $scenario) { + $data['title'] = $scenario->getTitle(); + $data['feature'] = $feature->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 \Behat\Gherkin\Parser + * 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(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(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) { + Scenario::load($bsid)->delete(); +} + +/** + * De-registers all scenario registrations for a given module. + * + * @param Drupal\Core\Extension\Extension $module + * The module to deregister. + */ +function behat_deregister_module_scenarios(Extension $module) { + $scenario_registrations = behat_get_module_scenario_registrations($module); + $entity_ids = array_keys($scenario_registrations); + + 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 = UrlHelper::buildQuery($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); + $output_dest = \Drupal::service('file_system')->realpath($dest_dir); + $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::service('file_system')->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..fad3e5f --- /dev/null +++ b/core/modules/behat/src/Entity/Scenario.php @@ -0,0 +1,89 @@ +setLabel(t('Behat Scenario ID')) + ->setDescription(t('The Behat scenario ID.')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + $fields['blid'] = BaseFieldDefinition::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'] = BaseFieldDefinition::create('uuid') + ->setLabel(t('UUID')) + ->setDescription(t('The Behat scenario UUID.')) + ->setReadOnly(TRUE); + + $fields['feature'] = BaseFieldDefinition::create('string') + ->setLabel(t('Feature')) + ->setDescription(t('The feature to which the Behat scenario belongs.')) + ->setSetting('default_value', ''); + + $fields['title'] = BaseFieldDefinition::create('string') + ->setLabel(t('Title')) + ->setDescription(t('The title of the Behat scenario.')) + ->setSetting('default_value', ''); + + $fields['location'] = BaseFieldDefinition::create('string') + ->setLabel(t('Location')) + ->setDescription(t('The location of the tests, relative to the Drupal base.')) + ->setSetting('default_value', ''); + + $fields['module'] = BaseFieldDefinition::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..6271503 --- /dev/null +++ b/core/modules/behat/src/Form/SettingsForm.php @@ -0,0 +1,73 @@ +config('behat.settings'); + + $form['log'] = [ + '#type' => 'select', + '#title' => t('Log destination'), + '#description' => t('Outputting to the Drupal database allows tests results to be viewed via Views.'), + '#options' => [ + '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, FormStateInterface $form_state) { + $this->config('behat.settings') + ->set('format', $form_state['values']['log']) + ->save(); + + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['behat.settings']; + } + +} diff --git a/core/modules/behat/src/ScenarioStorage.php b/core/modules/behat/src/ScenarioStorage.php new file mode 100644 index 0000000..e7b7c01 --- /dev/null +++ b/core/modules/behat/src/ScenarioStorage.php @@ -0,0 +1,27 @@ +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 $/ + */ + 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 $/ + */ + 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 $/ + */ + 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 $/ + */ + 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..2b06de0 --- /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 my "article" 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 100644 index 0000000..af66018 --- /dev/null +++ b/core/scripts/run-behat-tests.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env php +