first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+12
View File
File diff suppressed because one or more lines are too long
+33
View File
@@ -0,0 +1,33 @@
Description of axe import into Moodle
1/ Download the latest axe code somewhere (example /tmp/axe) using:
mkdir -p /tmp/axe
cd /tmp/axe
npm install axe-core --save-dev
Note down the version number displayed by the command, to update lib/thirdpartylibs.xml accordingly.
If the command does not output a version number, the version number can be found in package.json. You can simply open package.json
using your desired editor and look for the version number of axe-core. Alternatively, you may use the following commands:
- MacOS:
cat package.json | grep axe-core
- Linux:
cat package.json | grep '"version"' | awk -F '"' '{print $4}'
or
jq -r '.version' package.json
2/ Copy the following file to your local Moodle directory, to replace the old one:
cp /tmp/axe/node_modules/axe-core/axe.min.js [PATH TO YOUR MOODLE]/lib/behat/axe/
3/ Update lib/thirdpartylibs.xml with the new version number.
4/ Update the PHPDoc block of \behat_accessibility::run_axe_validation_for_tags() in
[PATH TO YOUR MOODLE]/lib/tests/behat/behat_accessibility.php to reflect the new version number.
5/ Run behat tests labelled with @accessibility and confirm they are passing with the new library version, or fix the failures
because the new version might raise issues that weren't detected previously:
php admin/tool/behat/cli/init.php --add-core-features-to-theme
php admin/tool/behat/cli/run.php --tags=@accessibility
+56
View File
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class of all steps definitions.
*
* This script is only called from Behat as part of it's integration
* in Moodle.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/classes/behat_session_interface.php');
require_once(__DIR__ . '/classes/behat_session_trait.php');
/**
* Steps definitions base class.
*
* To extend by the steps definitions of the different Moodle components.
*
* It can not contain steps definitions to avoid duplicates, only utility
* methods shared between steps.
*
* @method NodeElement find_field(string $locator) Finds a form element
* @method NodeElement find_button(string $locator) Finds a form input submit element or a button
* @method NodeElement find_link(string $locator) Finds a link on a page
* @method NodeElement find_file(string $locator) Finds a forum input file element
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_base extends Behat\MinkExtension\Context\RawMinkContext implements behat_session_interface {
// All of the functionality of behat_base is shared with form fields via the behat_session_trait trait.
use behat_session_trait;
}
+75
View File
@@ -0,0 +1,75 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_base.php');
/**
* Base class for steps definitions classes that contain deprecated steps.
*
* To be extended by the deprecated steps definitions of the different Moodle components and add-ons.
* For example, deprecated core steps can be found in lib/tests/behat/behat_deprecated.php ,
* deprecated steps for mod_forum would be in mod/forum/tests/behat/behat_mod_forum_deprecated.php etc.
*
* @package core
* @category test
* @copyright 2022 Marina Glancy
* @author David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_deprecated_base extends behat_base {
/**
* Throws an exception if $CFG->behat_usedeprecated is not allowed.
*
* @throws Exception
* @param string|array $alternatives Alternative/s to the requested step
* @param bool $throwexception If set to true we always throw exception, irrespective of behat_usedeprecated setting.
* @return void
*/
protected function deprecated_message($alternatives, bool $throwexception = false): void {
global $CFG;
// We do nothing if it is enabled.
if (!empty($CFG->behat_usedeprecated) && !$throwexception) {
return;
}
if (is_scalar($alternatives)) {
$alternatives = array($alternatives);
}
// Show an appropriate message based on the throwexception flag.
if ($throwexception) {
$message = 'This step has been removed. Rather than using this step you can:';
} else {
$message = 'Deprecated step, rather than using this step you can:';
}
// Add all alternatives to the message.
foreach ($alternatives as $alternative) {
$message .= PHP_EOL . '- ' . $alternative;
}
if (!$throwexception) {
$message .= PHP_EOL . '- Set $CFG->behat_usedeprecated in config.php to allow the use of deprecated steps
if you don\'t have any other option';
}
throw new Exception($message);
}
}
+343
View File
@@ -0,0 +1,343 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Form fields helper.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Behat\Mink\Session as Session,
Behat\Mink\Element\NodeElement as NodeElement,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
Behat\MinkExtension\Context\RawMinkContext as RawMinkContext;
/**
* Helper to interact with form fields.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_field_manager {
/**
* Gets an instance of the form field from it's label
*
* @param string $label
* @param RawMinkContext $context
* @return behat_form_field
*/
public static function get_form_field_from_label($label, RawMinkContext $context) {
// There are moodle form elements that are not directly related with
// a basic HTML form field, we should also take care of them.
// The DOM node.
$fieldnode = $context->find_field($label);
// The behat field manager.
$field = self::get_form_field($fieldnode, $context->getSession());
return $field;
}
/**
* Gets an instance of the form field.
*
* Not all the fields are part of a moodle form, in this
* cases it fallsback to the generic form field. Also note
* that this generic field type is using a generic setValue()
* method from the Behat API, which is not always good to set
* the value of form elements.
*
* @param NodeElement $fieldnode
* @param Session $session The behat browser session
* @return behat_form_field
*/
public static function get_form_field(NodeElement $fieldnode, Session $session) {
// Get the field type if is part of a moodleform.
if (self::is_moodleform_field($fieldnode)) {
$type = self::get_field_node_type($fieldnode, $session);
}
// If is not a moodleforms field use the base field type.
if (empty($type)) {
$type = 'field';
}
return self::get_field_instance($type, $fieldnode, $session);
}
/**
* Returns the appropiate behat_form_field according to the provided type.
*
* It defaults to behat_form_field.
*
* @param string $type The field type (checkbox, date_selector, text...)
* @param NodeElement $fieldnode
* @param Session $session The behat session
* @return behat_form_field
*/
public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
global $CFG;
// If the field is not part of a moodleform, we should still try to find out
// which field type are we dealing with.
if ($type == 'field' && $guessedtype = self::guess_field_type($fieldnode, $session)) {
$type = $guessedtype;
}
$classname = 'behat_form_' . $type;
// Fallsback on the type guesser if nothing specific exists.
$classpath = $CFG->libdir . '/behat/form_field/' . $classname . '.php';
if (!file_exists($classpath)) {
$classname = 'behat_form_field';
$classpath = $CFG->libdir . '/behat/form_field/' . $classname . '.php';
}
// Returns the instance.
require_once($classpath);
return new $classname($session, $fieldnode);
}
/**
* Guesses a basic field type and returns it.
*
* This method is intended to detect HTML form fields when no
* moodleform-specific elements have been detected.
*
* @param NodeElement $fieldnode
* @param Session $session
* @return string|bool The field type or false.
*/
public static function guess_field_type(NodeElement $fieldnode, Session $session) {
[
'document' => $document,
'node' => $node,
] = self::get_dom_elements_for_node($fieldnode, $session);
// If the type is explicitly set on the element pointed to by the label - use it.
if ($fieldtype = $node->getAttribute('data-fieldtype')) {
return self::normalise_fieldtype($fieldtype);
}
// Textareas are considered text based elements.
$tagname = strtolower($node->nodeName);
if ($tagname == 'textarea') {
$xpath = new \DOMXPath($document);
// If there is an iframe with $id + _ifr there a TinyMCE editor loaded.
if ($xpath->query('//div[@id="' . $node->getAttribute('id') . 'editable"]')->count() !== 0) {
return 'editor';
}
return 'textarea';
}
if ($tagname == 'input') {
switch ($node->getAttribute('type')) {
case 'text':
case 'password':
case 'email':
case 'file':
return 'text';
case 'checkbox':
return 'checkbox';
break;
case 'radio':
return 'radio';
break;
default:
// Here we return false because all text-based
// fields should be included in the first switch case.
return false;
}
}
if ($tagname == 'select') {
// Select tag.
return 'select';
}
if ($tagname == 'span') {
if ($node->hasAttribute('data-inplaceeditable') && $node->getAttribute('data-inplaceeditable')) {
// Determine appropriate editable type of this field (text or select).
if ($node->getAttribute('data-type') == 'select') {
return 'inplaceeditable_select';
} else {
return 'inplaceeditable';
}
}
}
if ($tagname == 'div') {
if ($node->getAttribute('role') == 'combobox') {
return 'select_menu';
}
}
// We can not provide a closer field type.
return false;
}
/**
* Detects when the field is a moodleform field type.
*
* Note that there are fields inside moodleforms that are not
* moodleform element; this method can not detect this, this will
* be managed by get_field_node_type, after failing to find the form
* element element type.
*
* @param NodeElement $fieldnode
* @return bool
*/
protected static function is_moodleform_field(NodeElement $fieldnode) {
// We already waited when getting the NodeElement and we don't want an exception if it's not part of a moodleform.
$parentformfound = $fieldnode->find('xpath',
"/ancestor::form[contains(concat(' ', normalize-space(@class), ' '), ' mform ')]"
);
return ($parentformfound != false);
}
/**
* Get the DOMDocument and DOMElement for a NodeElement.
*
* @param NodeElement $fieldnode
* @param Session $session
* @return array
*/
protected static function get_dom_elements_for_node(NodeElement $fieldnode, Session $session): array {
$html = $session->getPage()->getContent();
$document = new \DOMDocument();
$previousinternalerrors = libxml_use_internal_errors(true);
$document->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_BIGLINES);
libxml_clear_errors();
libxml_use_internal_errors($previousinternalerrors);
$xpath = new \DOMXPath($document);
$node = $xpath->query($fieldnode->getXpath())->item(0);
return [
'document' => $document,
'node' => $node,
];
}
/**
* Recursive method to find the field type.
*
* Depending on the field the felement class node is in a level or in another. We
* look recursively for a parent node with a 'felement' class to find the field type.
*
* @param NodeElement $fieldnode The current node.
* @param Session $session The behat browser session
* @return null|string A text description of the node type, or null if one could not be accurately determined
*/
protected static function get_field_node_type(NodeElement $fieldnode, Session $session): ?string {
[
'document' => $document,
'node' => $node,
] = self::get_dom_elements_for_node($fieldnode, $session);
return self::get_field_type($document, $node, $session);
}
/**
* Get the field type from the specified DOMElement.
*
* @param \DOMDocument $document
* @param \DOMElement $node
* @param Session $session
* @return null|string
*/
protected static function get_field_type(\DOMDocument $document, \DOMElement $node, Session $session): ?string {
$xpath = new \DOMXPath($document);
if ($node->getAttribute('name') === 'availabilityconditionsjson') {
// Special handling for availability field which requires custom JavaScript.
return 'availability';
}
if ($node->nodeName == 'html') {
// The top of the document has been reached.
return null;
}
// If the type is explictly set on the element pointed to by the label - use it.
$fieldtype = $node->getAttribute('data-fieldtype');
if ($fieldtype) {
return self::normalise_fieldtype($fieldtype);
}
if ($xpath->query('/ancestor::*[@data-passwordunmaskid]', $node)->count() !== 0) {
// This element has a passwordunmaskid as a parent.
return 'passwordunmask';
}
// Fetch the parentnode only once.
$parentnode = $node->parentNode;
if ($parentnode instanceof \DOMDocument) {
return null;
}
// Check the parent fieldtype before we check classes.
$fieldtype = $parentnode->getAttribute('data-fieldtype');
if ($fieldtype) {
return self::normalise_fieldtype($fieldtype);
}
// We look for a parent node with 'felement' class.
if ($class = $parentnode->getAttribute('class')) {
if (strstr($class, 'felement') != false) {
// Remove 'felement f' from class value.
return substr($class, 10);
}
// Stop propagation through the DOM, if it does not have a felement is not part of a moodle form.
if (strstr($class, 'fcontainer') != false) {
return null;
}
}
// Move up the tree.
return self::get_field_type($document, $parentnode, $session);
}
/**
* Normalise the field type.
*
* @param string $fieldtype
* @return string
*/
protected static function normalise_fieldtype(string $fieldtype): string {
if ($fieldtype === 'tags') {
return 'autocomplete';
}
return $fieldtype;
}
}
+278
View File
@@ -0,0 +1,278 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat command utils
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../lib.php');
/**
* Behat command related utils
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_command {
/**
* Docs url
*/
const DOCS_URL = 'https://moodledev.io/general/development/tools/behat';
/**
* Ensures the behat dir exists in moodledata
*
* @return string Full path
*/
public static function get_parent_behat_dir() {
global $CFG;
// If not set then return empty string.
if (!isset($CFG->behat_dataroot_parent)) {
return "";
}
return $CFG->behat_dataroot_parent;
}
/**
* Ensures the behat dir exists in moodledata
* @param int $runprocess run process for which behat dir is returned.
* @return string Full path
*/
public static function get_behat_dir($runprocess = 0) {
global $CFG;
// If not set then return empty string.
if (!isset($CFG->behat_dataroot)) {
return "";
}
// If $CFG->behat_parallel_run starts with index 0 and $runprocess for parallel run starts with 1.
if (!empty($runprocess) && isset($CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'])) {
$behatdir = $CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'] . '/behat';;
} else {
$behatdir = $CFG->behat_dataroot . '/behat';
}
if (!is_dir($behatdir)) {
if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Directory ' . $behatdir . ' can not be created');
}
}
if (!is_writable($behatdir)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Directory ' . $behatdir . ' is not writable');
}
return $behatdir;
}
/**
* Returns the executable path
*
* Allows returning a customized command for cygwin when the
* command is just displayed, when using exec(), system() and
* friends we stay with DIRECTORY_SEPARATOR as they use the
* normal cmd.exe (in Windows).
*
* @param bool $custombyterm If the provided command should depend on the terminal where it runs
* @param bool $parallelrun If parallel run is installed.
* @param bool $absolutepath return command with absolute path.
* @return string
*/
final public static function get_behat_command($custombyterm = false, $parallerun = false, $absolutepath = false) {
$separator = DIRECTORY_SEPARATOR;
$exec = 'behat';
// Cygwin uses linux-style directory separators.
if ($custombyterm && testing_is_cygwin()) {
$separator = '/';
// MinGW can not execute .bat scripts.
if (!testing_is_mingw()) {
$exec = 'behat.bat';
}
}
// If relative path then prefix relative path.
if ($absolutepath) {
$pathprefix = testing_cli_argument_path('/');
if (!empty($pathprefix)) {
$pathprefix .= $separator;
}
} else {
$pathprefix = '';
}
if (!$parallerun) {
$command = $pathprefix . 'vendor' . $separator . 'bin' . $separator . $exec;
} else {
$command = 'php ' . $pathprefix . 'admin' . $separator . 'tool' . $separator . 'behat' . $separator . 'cli'
. $separator . 'run.php';
}
return $command;
}
/**
* Runs behat command with provided options
*
* Execution continues when the process finishes
*
* @param string $options Defaults to '' so tests would be executed
* @return array CLI command outputs [0] => string, [1] => integer
*/
final public static function run($options = '') {
global $CFG;
$currentcwd = getcwd();
chdir($CFG->dirroot);
exec(self::get_behat_command() . ' ' . $options, $output, $code);
chdir($currentcwd);
return array($output, $code);
}
/**
* Checks if behat is set up and working
*
* Notifies failures both from CLI and web interface.
*
* It checks behat dependencies have been installed and runs
* the behat help command to ensure it works as expected
*
* @return int Error code or 0 if all ok
*/
public static function behat_setup_problem() {
global $CFG;
// Moodle setting.
if (!self::are_behat_dependencies_installed()) {
// Returning composer error code to avoid conflicts with behat and moodle error codes.
self::output_msg(get_string('errorcomposer', 'tool_behat'));
return TESTING_EXITCODE_COMPOSER;
}
// Behat test command.
$dirrootconfigpath = $CFG->dirroot . DIRECTORY_SEPARATOR . 'behat.yml';
if (file_exists($dirrootconfigpath)) {
self::output_msg(get_string('warndirrootconfigfound', 'tool_behat', $dirrootconfigpath));
}
list($output, $code) = self::run(" --help");
if ($code != 0) {
// Returning composer error code to avoid conflicts with behat and moodle error codes.
self::output_msg(get_string('errorbehatcommand', 'tool_behat', self::get_behat_command()));
return TESTING_EXITCODE_COMPOSER;
}
// No empty values.
if (empty($CFG->behat_dataroot) || empty($CFG->behat_prefix) || empty($CFG->behat_wwwroot)) {
self::output_msg(get_string('errorsetconfig', 'tool_behat'));
return BEHAT_EXITCODE_CONFIG;
}
// Not repeated values.
// We only need to check this when the behat site is not running as
// at this point, when it is running, all $CFG->behat_* vars have
// already been copied to $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
$phpunitprefix = empty($CFG->phpunit_prefix) ? '' : $CFG->phpunit_prefix;
$behatdbname = empty($CFG->behat_dbname) ? $CFG->dbname : $CFG->behat_dbname;
$phpunitdbname = empty($CFG->phpunit_dbname) ? $CFG->dbname : $CFG->phpunit_dbname;
$behatdbhost = empty($CFG->behat_dbhost) ? $CFG->dbhost : $CFG->behat_dbhost;
$phpunitdbhost = empty($CFG->phpunit_dbhost) ? $CFG->dbhost : $CFG->phpunit_dbhost;
$samedataroot = $CFG->behat_dataroot == $CFG->dataroot;
$samedataroot = $samedataroot || (!empty($CFG->phpunit_dataroot) && $CFG->phpunit_dataroot == $CFG->behat_dataroot);
$samewwwroot = $CFG->behat_wwwroot == $CFG->wwwroot;
$sameprefix = ($CFG->behat_prefix == $CFG->prefix && $behatdbname == $CFG->dbname && $behatdbhost == $CFG->dbhost);
$sameprefix = $sameprefix || ($CFG->behat_prefix == $phpunitprefix && $behatdbname == $phpunitdbname &&
$behatdbhost == $phpunitdbhost);
if (!defined('BEHAT_SITE_RUNNING') && ($samedataroot || $samewwwroot || $sameprefix)) {
self::output_msg(get_string('erroruniqueconfig', 'tool_behat'));
return BEHAT_EXITCODE_CONFIG;
}
// Checking behat dataroot existence otherwise echo about admin/tool/behat/cli/init.php.
if (!empty($CFG->behat_dataroot)) {
$CFG->behat_dataroot = realpath($CFG->behat_dataroot);
}
if (empty($CFG->behat_dataroot) || !is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
self::output_msg(get_string('errordataroot', 'tool_behat'));
return BEHAT_EXITCODE_CONFIG;
}
return 0;
}
/**
* Has the site installed composer.
* @return bool
*/
public static function are_behat_dependencies_installed() {
if (!is_dir(__DIR__ . '/../../../vendor/behat')) {
return false;
}
return true;
}
/**
* Outputs a message.
*
* Used in CLI + web UI methods. Stops the
* execution in web.
*
* @param string $msg
* @return void
*/
protected static function output_msg($msg) {
global $CFG, $PAGE;
// If we are using the web interface we want pretty messages.
if (!CLI_SCRIPT) {
$renderer = $PAGE->get_renderer('tool_behat');
echo $renderer->render_error($msg);
// Stopping execution.
exit(1);
} else {
// We continue execution after this.
$clibehaterrorstr = "Ensure you set \$CFG->behat_* vars in config.php " .
"and you ran admin/tool/behat/cli/init.php.\n" .
"More info in " . self::DOCS_URL;
echo 'Error: ' . $msg . "\n\n" . $clibehaterrorstr;
}
}
}
+292
View File
@@ -0,0 +1,292 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Utils to set Behat config
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/behat_config_util.php');
/**
* Behat configuration manager
*
* Creates/updates Behat config files getting tests
* and steps from Moodle codebase
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_config_manager {
/**
* @var bool Keep track of the automatic profile conversion. So we can notify user.
*/
public static $autoprofileconversion = false;
/**
* @var behat_config_util keep object of behat_config_util for use.
*/
public static $behatconfigutil = null;
/**
* Returns behat_config_util.
*
* @return behat_config_util
*/
private static function get_behat_config_util() {
if (!self::$behatconfigutil) {
self::$behatconfigutil = new behat_config_util();
}
return self::$behatconfigutil;
}
/**
* Updates a config file
*
* The tests runner and the steps definitions list uses different
* config files to avoid problems with concurrent executions.
*
* The steps definitions list can be filtered by component so it's
* behat.yml is different from the $CFG->dirroot one.
*
* @param string $component Restricts the obtained steps definitions to the specified component
* @param string $testsrunner If the config file will be used to run tests
* @param string $tags features files including tags.
* @param bool $themesuitewithallfeatures if only theme specific features need to be included in the suite.
* @param int $parallelruns number of parallel runs.
* @param int $run current run for which config needs to be updated.
* @return void
*/
public static function update_config_file($component = '', $testsrunner = true, $tags = '',
$themesuitewithallfeatures = false, $parallelruns = 0, $run = 0) {
global $CFG;
// Behat must have a separate behat.yml to have access to the whole set of features and steps definitions.
if ($testsrunner === true) {
$configfilepath = behat_command::get_behat_dir($run) . '/behat.yml';
} else {
// Alternative for steps definitions filtering, one for each user.
$configfilepath = self::get_steps_list_config_filepath();
}
$behatconfigutil = self::get_behat_config_util();
$behatconfigutil->set_theme_suite_to_include_core_features($themesuitewithallfeatures);
$behatconfigutil->set_tag_for_feature_filter($tags);
// Gets all the components with features, if running the tests otherwise not required.
$features = array();
if ($testsrunner) {
$features = $behatconfigutil->get_components_features();
}
// Gets all the components with steps definitions.
$stepsdefinitions = $behatconfigutil->get_components_contexts($component);
if (!$testsrunner) {
// Exclude deprecated steps definitions from the available steps list.
foreach (array_keys($stepsdefinitions) as $key) {
if (preg_match('/_deprecated$/', $key)) {
unset($stepsdefinitions[$key]);
}
}
}
// Get current run.
if (empty($run) && ($run !== false) && !empty($CFG->behatrunprocess)) {
$run = $CFG->behatrunprocess;
}
// Get number of parallel runs if not passed.
if (empty($parallelruns) && ($parallelruns !== false)) {
$parallelruns = self::get_behat_run_config_value('parallel');
}
// Behat config file specifing the main context class,
// the required Behat extensions and Moodle test wwwroot.
$contents = $behatconfigutil->get_config_file_contents($features, $stepsdefinitions, $tags, $parallelruns, $run);
// Stores the file.
if (!file_put_contents($configfilepath, $contents)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $configfilepath . ' can not be created');
}
}
/**
* Returns the behat config file path used by the steps definition list
*
* @return string
*/
public static function get_steps_list_config_filepath() {
global $USER;
// We don't cygwin-it as it is called using exec() which uses cmd.exe.
$userdir = behat_command::get_behat_dir() . '/users/' . $USER->id;
make_writable_directory($userdir);
return $userdir . '/behat.yml';
}
/**
* Returns the behat config file path used by the behat cli command.
*
* @param int $runprocess Runprocess.
* @return string
*/
public static function get_behat_cli_config_filepath($runprocess = 0) {
global $CFG;
if ($runprocess) {
if (isset($CFG->behat_parallel_run[$runprocess - 1 ]['behat_dataroot'])) {
$command = $CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'];
} else {
$command = $CFG->behat_dataroot . $runprocess;
}
} else {
$command = $CFG->behat_dataroot;
}
$command .= DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 'behat.yml';
// Cygwin uses linux-style directory separators.
if (testing_is_cygwin()) {
$command = str_replace('\\', '/', $command);
}
return $command;
}
/**
* Returns the path to the parallel run file which specifies if parallel test environment is enabled
* and how many parallel runs to execute.
*
* @return string
*/
final public static function get_behat_run_config_file_path() {
return behat_command::get_parent_behat_dir() . '/run_environment.json';
}
/**
* Get config for parallel run.
*
* @param string $key Key to store
* @return string|int|array value which is stored.
*/
final public static function get_behat_run_config_value($key) {
$parallelrunconfigfile = self::get_behat_run_config_file_path();
if (file_exists($parallelrunconfigfile)) {
if ($parallelrunconfigs = @json_decode(file_get_contents($parallelrunconfigfile), true)) {
if (isset($parallelrunconfigs[$key])) {
return $parallelrunconfigs[$key];
}
}
}
return false;
}
/**
* Save/update config for parallel run.
*
* @param string $key Key to store
* @param string|int|array $value to store.
*/
final public static function set_behat_run_config_value($key, $value) {
$parallelrunconfigs = array();
$parallelrunconfigfile = self::get_behat_run_config_file_path();
// Get any existing config first.
if (file_exists($parallelrunconfigfile)) {
$parallelrunconfigs = @json_decode(file_get_contents($parallelrunconfigfile), true);
}
$parallelrunconfigs[$key] = $value;
@file_put_contents($parallelrunconfigfile, json_encode($parallelrunconfigs, JSON_PRETTY_PRINT));
}
/**
* Drops parallel site links.
*
* @return bool true on success else false.
*/
final public static function drop_parallel_site_links() {
global $CFG;
// Get parallel test runs.
$parallelrun = self::get_behat_run_config_value('parallel');
if (empty($parallelrun)) {
return false;
}
// If parallel run then remove links and original file.
clearstatcache();
for ($i = 1; $i <= $parallelrun; $i++) {
// Don't delete links for specified sites, as they should be accessible.
if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) {
continue;
}
$link = $CFG->dirroot . '/' . BEHAT_PARALLEL_SITE_NAME . $i;
if (file_exists($link) && is_link($link)) {
@unlink($link);
}
}
return true;
}
/**
* Create parallel site links.
*
* @param int $fromrun first run
* @param int $torun last run.
* @return bool true for sucess, else false.
*/
final public static function create_parallel_site_links($fromrun, $torun) {
global $CFG;
// Create site symlink if necessary.
clearstatcache();
for ($i = $fromrun; $i <= $torun; $i++) {
// Don't create links for specified sites, as they should be accessible.
if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) {
continue;
}
$link = $CFG->dirroot.'/'.BEHAT_PARALLEL_SITE_NAME.$i;
clearstatcache();
if (file_exists($link)) {
if (!is_link($link) || !is_dir($link)) {
echo "File exists at link location ($link) but is not a link or directory!" . PHP_EOL;
return false;
}
} else if (!symlink($CFG->dirroot, $link)) {
// Try create link in case it's not already present.
echo "Unable to create behat site symlink ($link)" . PHP_EOL;
return false;
}
}
return true;
}
}
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Helper to get behat contexts from other contexts.
*
* @package core
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Behat\Testwork\Environment\Environment;
use Behat\Mink\Exception\DriverException;
/**
* Helper to get behat contexts.
*
* @package core
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_context_helper {
/**
* Behat environment.
*
* @var Environment
*/
protected static $environment = null;
/**
* @var Escaper::escapeLiteral
*/
protected static $escaper;
/**
* @var array keep track of nonexisting contexts, to avoid exception tracking.
*/
protected static $nonexistingcontexts = array();
/**
* Sets behat environment.
*
* @param Environment $environment
* @return void
*/
public static function set_environment(Environment $environment) {
self::$environment = $environment;
}
/**
* Gets the required context.
*
* Getting a context you get access to all the steps
* that uses direct API calls; steps returning step chains
* can not be executed like this.
*
* @throws Behat\Behat\Context\Exception\ContextNotFoundException
* @param string $classname Context identifier (the class name).
* @return behat_base
*/
public static function get($classname) {
$definedclassname = self::get_theme_override($classname);
if ($definedclassname) {
return self::$environment->getContext($definedclassname);
}
// Just fall back on getContext to ensure that we throw the correct exception.
return self::$environment->getContext($classname);
}
/**
* Get the context for the specified component or subsystem.
*
* @param string $component The component or subsystem to find the context for
* @return behat_base|null
*/
public static function get_component_context(string $component): ?behat_base {
$component = str_replace('core_', '', $component);
if ($classname = self::get_theme_override("behat_{$component}")) {
return self::get($classname);
}
return null;
}
/**
* Find all Behat contexts which match the specified context class name prefix.
*
* Moodle uses a consistent class naming scheme for all Behat contexts, whereby the context name is in the format:
*
* behat_{component}
*
* This method will return all contexts which match the specified prefix.
*
* For example, to find all editors, you would pass in 'behat_editor', and this might return:
* - behat_editor_atto
* - behat_editor_textarea
*
* @param string $prefix The prefix to search for
* @return \Behat\Behat\Context\Context[]
*/
public static function get_prefixed_contexts(string $prefix): array {
if (!is_a(self::$environment, \Behat\Behat\Context\Environment\InitializedContextEnvironment::class)) {
throw new DriverException(
'Cannot get prefixed contexts - the environment is not an InitializedContextEnvironment'
);
}
return array_filter(self::$environment->getContexts(), function($context) use ($prefix): bool {
return (strpos(get_class($context), $prefix) === 0);
});
}
/**
* Check for any theme override of the specified class name.
*
* @param string $classname
* @return string|null
*/
protected static function get_theme_override(string $classname): ?string {
$suitename = self::$environment->getSuite()->getName();
// If default suite, then get the default theme name.
if ($suitename == 'default') {
$suitename = theme_config::DEFAULT_THEME;
}
$overrideclassname = "behat_theme_{$suitename}_{$classname}";
if (self::$environment->hasContextClass($overrideclassname)) {
return $overrideclassname;
}
if (self::$environment->hasContextClass($classname)) {
return $classname;
}
return null;
}
/**
* Return whether there is a context of the specified classname.
*
* @param string $classname
* @return bool
*/
public static function has_context(string $classname): bool {
return self::$environment->hasContextClass($classname);
}
/**
* Translates string to XPath literal.
*
* @param string $label label to escape
* @return string escaped string.
*/
public static function escape($label) {
if (empty(self::$escaper)) {
self::$escaper = new \Behat\Mink\Selector\Xpath\Escaper();
}
return self::$escaper->escapeLiteral($label);
}
}
File diff suppressed because it is too large Load Diff
+626
View File
@@ -0,0 +1,626 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for data generators component support for acceptance testing.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Behat\Tester\Exception\PendingException as PendingException;
/**
* Class to quickly create Behat test data using component data generators.
*
* There is a subclass of class for each component that wants to be able to
* generate entities using the Behat step
* Given the following "entity types" exist:
* | test | data |
*
* For core entities, the entity type is like "courses" or "users" and
* generating those is handled by behat_core_generator. For other components
* the entity type is like "mod_quiz > User override" and that is handled by
* behat_mod_quiz_generator defined in mod/quiz/tests/generator/behat_mod_quiz_generator.php.
*
* The types of entities that can be generated are described by the array returned
* by the {@link get_generateable_entities()} method. The list in
* {@link behat_core_generator} is a good (if complex) example.
*
* How things work is best explained with a few examples. All this is implemented
* in the {@link generate_items()} method below, if you want to see every detail of
* how it works.
*
* Simple example from behat_core_generator:
* 'users' => [
* 'datagenerator' => 'user',
* 'required' => ['username'],
* ],
* The steps performed are:
*
* 1. 'datagenerator' => 'user' means that the word used in the method names below is 'user'.
*
* 2. Because 'required' is present, check the supplied data exists 'username' column is present
* in the supplied data table and if not display an error.
*
* 3. Then for each row in the table as an array $elementdata (array keys are column names)
* and process it as follows
*
* 4. (Not used in this example.)
*
* 5. If the method 'preprocess_user' exists, then call it to update $elementdata.
* (It does, in this case it sets the password to the username, if password was not given.)
*
* We then do one of 4 things:
*
* 6a. If there is a method 'process_user' we call it. (It doesn't for user,
* but there are other examples like process_enrol_user() in behat_core_generator.)
*
* 6b. (Not used in this example.)
*
* 6c. Else, if testing_data_generator::create_user exists, we call it with $elementdata. (it does.)
*
* 6d. If none of these three things work. an error is thrown.
*
* To understand the missing steps above, consider the example from behat_mod_quiz_generator:
* 'group override' => [
* 'datagenerator' => 'override',
* 'required' => ['quiz', 'group'],
* 'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
* ],
* Processing is as above, except that:
*
* 1. Note 'datagenerator' is 'override' (not group_override). 'user override' maps to the
* same datagenerator. This works fine.
*
* 4. Because 'switchids' is present, human-readable data in the table gets converted to ids.
* They array key 'group' refers to a column which may be present in the table (it will be
* here because it is required, but it does not have to be in general). If that column
* is present and contains a value, then the method matching name like get_group_id() is
* called with the value from that column in the data table. You must implement this
* method. You can see several examples of this sort of method below.
*
* If that method returns a group id, then $elementdata['group'] is unset and
* $elementdata['groupid'] is set to the result of the get_group_id() call. 'groupid' here
* because of the definition is 'switchids' => [..., 'group' => 'groupid'].
* If get_group_id() cannot find the group, it should throw a helpful exception.
*
* Similarly, 'quiz' (the quiz name) is looked up with a call to get_quiz_id(). Here, the
* new array key set matches the old one removed. This is fine.
*
* 6b. We are in a plugin, so before checking whether testing_data_generator::create_override
* exists we first check whether mod_quiz_generator::create_override() exists. It does,
* and this is what gets called.
*
* This second example shows why the get_..._id methods for core entities are in this base
* class, not in behat_core_generator. Plugins may need to look up the ids of
* core entities.
*
* behat_core_generator is defined in lib/behat/classes/behat_core_generator.php
* and for components, behat_..._generator is defined in tests/generator/behat_..._generator.php
* inside the plugin. For example behat_mod_quiz_generator is defined in
* mod/quiz/tests/generator/behat_mod_quiz_generator.php.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class behat_generator_base {
/**
* @var string the name of the component we belong to.
*
* This should probably only be used to make error messages clearer.
*/
protected $component;
/**
* @var testing_data_generator the core data generator
*/
protected $datagenerator;
/**
* @var testing_data_generator the data generator for this component.
*/
protected $componentdatagenerator;
/**
* Constructor.
*
* @param string $component component name, to make error messages more readable.
*/
public function __construct(string $component) {
$this->component = $component;
}
/**
* Get a list of the entities that can be created for this component.
*
* This function must be overridden in subclasses. See class comment
* above for a description of the data structure.
* See {@link behat_core_generator} for an example.
*
* @return array entity name => information about how to generate.
*/
abstract protected function get_creatable_entities(): array;
/**
* Get the list of available generators for this class.
*
* @return array
*/
final public function get_available_generators(): array {
return $this->get_creatable_entities();
}
/**
* Do the work to generate an entity.
*
* This is called by {@link behat_data_generators::the_following_entities_exist()}.
*
* @param string $generatortype The name of the entity to create.
* @param TableNode $data from the step.
* @param bool $singular Whether there is only one record and it is pivotted
*/
public function generate_items(string $generatortype, TableNode $data, bool $singular = false) {
// Now that we need them require the data generators.
require_once(__DIR__ . '/../../testing/generator/lib.php');
$elements = $this->get_creatable_entities();
foreach ($elements as $key => $configuration) {
if (array_key_exists('singular', $configuration)) {
$singularverb = $configuration['singular'];
unset($configuration['singular']);
unset($elements[$key]['singular']);
$elements[$singularverb] = $configuration;
}
}
if (!isset($elements[$generatortype])) {
throw new PendingException($this->name_for_errors($generatortype) .
' is not a known type of entity that can be generated.');
}
$entityinfo = $elements[$generatortype];
$this->datagenerator = testing_util::get_data_generator();
if ($this->component === 'core') {
$this->componentdatagenerator = $this->datagenerator;
} else {
$this->componentdatagenerator = $this->datagenerator->get_plugin_generator($this->component);
}
$generatortype = $entityinfo['datagenerator'];
if ($singular) {
// There is only one record to generate, and the table has been pivotted.
// The rows each represent a single field.
$rows = [$data->getRowsHash()];
} else {
// There are multiple records to generate.
// The rows represent an item to create.
$rows = $data->getHash();
}
foreach ($rows as $elementdata) {
// Check if all the required fields are there.
foreach ($entityinfo['required'] as $requiredfield) {
if (!isset($elementdata[$requiredfield])) {
throw new Exception($this->name_for_errors($generatortype) .
' requires the field ' . $requiredfield . ' to be specified');
}
}
// Switch from human-friendly references to ids.
if (!empty($entityinfo['switchids'])) {
foreach ($entityinfo['switchids'] as $element => $field) {
$methodname = 'get_' . $element . '_id';
// Not all the switch fields are required, default vars will be assigned by data generators.
if (isset($elementdata[$element])) {
if (!method_exists($this, $methodname)) {
throw new coding_exception('The generator for ' .
$this->name_for_errors($generatortype) .
' entities specifies \'switchids\' => [..., \'' . $element .
'\' => \'' . $field . '\', ...] but the required method ' .
$methodname . '() has not been defined in ' .
get_class($this) . '.');
}
// Temp $id var to avoid problems when $element == $field.
$id = $this->{$methodname}($elementdata[$element]);
unset($elementdata[$element]);
$elementdata[$field] = $id;
}
}
}
// Preprocess the entities that requires a special treatment.
if (method_exists($this, 'preprocess_' . $generatortype)) {
$elementdata = $this->{'preprocess_' . $generatortype}($elementdata);
}
// Creates element.
if (method_exists($this, 'process_' . $generatortype)) {
// Use a method on this class to do the work.
$this->{'process_' . $generatortype}($elementdata);
} else if (method_exists($this->componentdatagenerator, 'create_' . $generatortype)) {
// Using the component't own data generator if it exists.
$this->componentdatagenerator->{'create_' . $generatortype}($elementdata);
} else if (method_exists($this->datagenerator, 'create_' . $generatortype)) {
// Use a method on the core data geneator, if there is one.
$this->datagenerator->{'create_' . $generatortype}($elementdata);
} else {
// Give up.
throw new PendingException($this->name_for_errors($generatortype) .
' data generator is not implemented');
}
}
// Notify that the all the elements have been generated.
if (method_exists($this->componentdatagenerator, 'finish_generate_' . $generatortype)) {
// Using the component's own data generator if it exists.
$this->componentdatagenerator->{'finish_generate_' . $generatortype}();
} else if (method_exists($this->datagenerator, 'finish_generate_' . $generatortype)) {
// Use a method on the core data geneator, if there is one.
$this->datagenerator->{'finish_generate_' . $generatortype}();
}
}
/**
* Helper for formatting error messages.
*
* @param string $entitytype entity type without prefix, e.g. 'frog'.
* @return string either 'frog' for core entities, or 'mod_mymod > frog' for components.
*/
protected function name_for_errors(string $entitytype): string {
if ($this->component === 'core') {
return '"' . $entitytype . '"';
} else {
return '"' . $this->component . ' > ' . $entitytype . '"';
}
}
/**
* Gets the grade category id from the grade category fullname
*
* @param string $fullname the grade category name.
* @return int corresponding id.
*/
protected function get_gradecategory_id($fullname) {
global $DB;
if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
}
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_user_id($username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_userfrom_id(string $username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_userto_id(string $username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the role id from it's shortname.
* @throws Exception
* @param string $roleshortname
* @return int
*/
protected function get_role_id($roleshortname) {
global $DB;
if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
}
return $id;
}
/**
* Gets the category id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_category_id($idnumber) {
global $DB;
// If no category was specified use the data generator one.
if ($idnumber == false) {
return null;
}
if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the course id from it's shortname.
* @throws Exception
* @param string $shortname
* @return int
*/
protected function get_course_id($shortname) {
global $DB;
if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
}
return $id;
}
/**
* Gets the course cmid for the specified activity based on the activity's idnumber.
*
* Note: this does not check the module type, only the idnumber.
*
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_activity_id(string $idnumber) {
global $DB;
if (!$id = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber])) {
throw new Exception('The specified activity with idnumber "' . $idnumber . '" could not be found.');
}
return $id;
}
/**
* Gets the group id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_group_id($idnumber) {
global $DB;
if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the grouping id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_grouping_id($idnumber) {
global $DB;
// Do not fetch grouping ID for empty grouping idnumber.
if (empty($idnumber)) {
return null;
}
if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the cohort id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_cohort_id($idnumber) {
global $DB;
if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the outcome item id from its shortname.
* @throws Exception
* @param string $shortname
* @return int
*/
protected function get_outcome_id($shortname) {
global $DB;
if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
}
return $id;
}
/**
* Get the id of a named scale.
* @param string $name the name of the scale.
* @return int the scale id.
*/
protected function get_scale_id($name) {
global $DB;
if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
throw new Exception('The specified scale with name "' . $name . '" does not exist');
}
return $id;
}
/**
* Get the id of a named question category (must be globally unique).
* Note that 'Top' is a special value, used when setting the parent of another
* category, meaning top-level.
*
* @param string $name the question category name.
* @return int the question category id.
*/
protected function get_questioncategory_id($name) {
global $DB;
if ($name == 'Top') {
return 0;
}
if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
throw new Exception('The specified question category with name "' . $name . '" does not exist');
}
return $id;
}
/**
* Gets the internal context id from the context reference.
*
* The context reference changes depending on the context
* level, it can be the system, a user, a category, a course or
* a module.
*
* @throws Exception
* @param string $levelname The context level string introduced by the test writer
* @param string $contextref The context reference introduced by the test writer
* @return context
*/
protected function get_context($levelname, $contextref) {
return behat_base::get_context($levelname, $contextref);
}
/**
* Gets the contact id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_contact_id($username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the external backpack id from it's backpackweburl.
* @param string $backpackweburl
* @return mixed
* @throws dml_exception
*/
protected function get_externalbackpack_id($backpackweburl) {
global $DB;
if (!$id = $DB->get_field('badge_external_backpack', 'id', ['backpackweburl' => $backpackweburl])) {
throw new Exception('The specified external backpack with backpackweburl "' . $username . '" does not exist');
}
return $id;
}
/**
* Get a coursemodule from an activity name or idnumber.
*
* @param string $activity
* @param string $identifier
* @return cm_info
*/
protected function get_cm_by_activity_name(string $activity, string $identifier): cm_info {
global $DB;
$coursetable = new \core\dml\table('course', 'c', 'c');
$courseselect = $coursetable->get_field_select();
$coursefrom = $coursetable->get_from_sql();
$cmtable = new \core\dml\table('course_modules', 'cm', 'cm');
$cmfrom = $cmtable->get_from_sql();
$acttable = new \core\dml\table($activity, 'a', 'a');
$actselect = $acttable->get_field_select();
$actfrom = $acttable->get_from_sql();
$sql = <<<EOF
SELECT cm.id as cmid, {$courseselect}, {$actselect}
FROM {$cmfrom}
INNER JOIN {$coursefrom} ON c.id = cm.course
INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
INNER JOIN {$actfrom} ON cm.instance = a.id
WHERE cm.idnumber = :idnumber OR a.name = :name
EOF;
$result = $DB->get_record_sql($sql, [
'modname' => $activity,
'idnumber' => $identifier,
'name' => $identifier,
], MUST_EXIST);
$course = $coursetable->extract_from_result($result);
$instancedata = $acttable->extract_from_result($result);
return get_fast_modinfo($course)->get_cm($result->cmid);
}
}
+121
View File
@@ -0,0 +1,121 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle-specific selectors.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/named_selector.php');
require_once(__DIR__ . '/exact_named_selector.php');
require_once(__DIR__ . '/partial_named_selector.php');
use Behat\Mink\Exception\ExpectationException as ExpectationException;
use Behat\Mink\Element\Element;
/**
* Moodle selectors manager.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_selectors {
/**
* Returns the behat selector and locator for a given moodle selector and locator
*
* @param string $selectortype The moodle selector type, which includes moodle selectors
* @param string $element The locator we look for in that kind of selector
* @param Session $session The Mink opened session
* @return array Contains the selector and the locator expected by Mink.
*/
public static function get_behat_selector($selectortype, $element, Behat\Mink\Session $session) {
// Note: This function is not deprecated, but not the recommended way of doing things.
[
'selector' => $selector,
'locator' => $locator,
] = $session->normalise_selector($selectortype, $element, $session->getPage());
// CSS and XPath selectors locator is one single argument.
return [$selector, $locator];
}
/**
* Allowed selectors getter.
*
* @return array
*/
public static function get_allowed_selectors() {
return array_merge(
behat_partial_named_selector::get_allowed_selectors(),
behat_exact_named_selector::get_allowed_selectors()
);
}
/**
* Allowed text selectors getter.
*
* @return array
*/
public static function get_allowed_text_selectors() {
return array_merge(
behat_partial_named_selector::get_allowed_text_selectors(),
behat_exact_named_selector::get_allowed_text_selectors()
);
}
/**
* Normalise the selector and locator for a named partial.
*
* @param string $selector The selector name
* @param string $locator The value to normalise
* @return array
*/
public static function normalise_named_selector(string $selector, string $locator): array {
return [
$selector,
behat_context_helper::escape($locator),
];
}
/**
* Transform the selector for a field.
*
* @param string $label The label to find
* @param Element $container The container to look within
* @return array The selector, locator, and container to search within
*/
public static function transform_find_for_field(behat_base $context, string $label, Element $container): array {
$hasfieldset = strpos($label, '>');
if (false !== $hasfieldset) {
[$containerlabel, $label] = explode(">", $label, 2);
$container = $context->find_fieldset(trim($containerlabel), $container);
$label = trim($label);
}
return [
'selector' => 'named_partial',
'locator' => self::normalise_named_selector('field', $label),
'container' => $container,
];
}
}
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The Interface for a behat root context.
*
* @package core
* @category test
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* The Interface for a behat root context.
*
* This interface should be implemented by the behat_base context, and behat form fields, and it should be paired with
* the behat_session_trait.
*
* It should not be necessary to implement this interface, and the behat_session_trait trait in normal circumstances.
*
* @package core
* @category test
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface behat_session_interface {
/**
* The JS code to check that the page is ready.
*
* The document must be complete and either M.util.pending_js must be empty, or it must not be defined at all.
*/
const PAGE_READY_JS = "document.readyState === 'complete' && " .
"(typeof M !== 'object' || typeof M.util !== 'object' || " .
"typeof M.util.pending_js === 'undefined' || M.util.pending_js.length === 0)";
/**
* Returns the Mink session.
*
* @param string|null $name name of the session OR active session will be used
* @return \Behat\Mink\Session
*/
public function getSession($name = null);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,99 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A class for recording the definition of Mink replacements.
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* A class for recording the definition of Mink replacements for use in Mink selectors.
*
* These are comprised of a source string, and a replacement.
*
* During use the source string is converted from the string to be in the format:
*
* %[component]/[string]%
*
* For example:
*
* %mod_forum/title%
*
* Mink replacements are used in xpath translation to translate regularly used items such as title.
* Here is an example from the upstream Mink project:
*
* '%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)'
*
* And can be used in an xpath:
*
* .//label[%tagTextMatch%]
*
* This would be expanded to:
*
* .//label[contains(normalize-space(string(.)), %locator%)]
*
* Replacements can also be used in other replacements, as long as that replacement is defined later.
*
* '%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)'
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_component_named_replacement {
/** @var string */
protected $from;
/** @var string */
protected $to;
/**
* Create the replacement.
*
* @param string $from this is the old selector that should no longer be used.
* For example 'group_message'.
* @param string $to this is the new equivalent that should be used instead.
* For example 'core_message > Message'.
*/
public function __construct(string $from, string $to) {
$this->from = $from;
$this->to = $to;
}
/**
* Get the 'from' part of the replacement, formatted for the component.
*
* @param string $component
* @return string
*/
public function get_from(string $component): string {
return "%{$component}/{$this->from}%";
}
/**
* Get the 'to' part of the replacement.
*
* @return string Target xpath
*/
public function get_to(): string {
return $this->to;
}
}
@@ -0,0 +1,124 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class representing a named selector that can be used in Behat tests.
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Class representing a named selector that can be used in Behat tests.
*
* Named selectors are what make Behat steps like
* Then I should see "Useful text" in the "General" "fieldset"
* Here, "fieldset" is the named selector, and "General" is the locator.
*
* Selectors can either be exact, in which case the locator needs to
* match exactly, or can be partial, for example the way
* When I click "Save" "button"
* will trigger a "Save changes" button.
*
* Instances of this class get returned by the get_exact_named_selectors()
* and get_partial_named_selectors() methods in classes like behat_mod_mymod.
* The code that makes the magic work is in the trait behat_named_selector
* used by both behat_exact_named_selector and behat_partial_named_selector.
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_component_named_selector {
/** @var string */
protected $alias;
/** @var array List of xpaths */
protected $xpaths;
/** @var string */
protected $istextselector;
/**
* Create the selector definition.
*
* As an example, if you define
* new behat_component_named_selector('Message',
* [".//*[@data-conversation-id]//img[contains(@alt, %locator%)]/.."])
* in get_partial_named_selectors in behat_message in
* message/tests/behat/behat_message.php, then steps like
* When "Group 1" "core_message > Message" should exist
* will work.
*
* Text selectors are things that contain other things (e.g. some particular text), e.g.
* Then I can see "Some text" in the "Whatever" "text_selector"
* whereas non-text selectors are atomic things, like
* When I click the "Whatever" "widget".
*
* @param string $alias The 'friendly' name of the thing. This will be prefixed with the component name.
* For example, if the mod_mymod plugin, says 'Thingy', then "mod_mymod > Thingy" becomes a selector.
* @param array $xpaths A list of xpaths one or more XPaths that the selector gets transformed into.
* @param bool $istextselector Whether this selector can also be used as a text selector.
*/
public function __construct(string $alias, array $xpaths, bool $istextselector = true) {
$this->alias = $alias;
$this->xpaths = $xpaths;
$this->istextselector = $istextselector;
}
/**
* Whether this is a text selector.
*
* @return bool
*/
public function is_text_selector(): bool {
return $this->istextselector;
}
/**
* Get the name of the selector.
* This is a back-end feature and contains a namespaced md5 of the human-readable name.
*
* @param string $component
* @return string
*/
public function get_name(string $component): string {
return implode('_', [$component, md5($this->alias)]);
}
/**
* Get the alias of the selector.
* This is the human-readable name that you would typically interact with.
*
* @param string $component
* @return string
*/
public function get_alias(string $component): string {
return implode(" > ", [$component, $this->alias]);;
}
/**
* Get the list of combined xpaths.
*
* @return string The list of xpaths combined with the xpath | (OR) operator
*/
public function get_combined_xpath(): string {
return implode(' | ', $this->xpaths);
}
}
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle-specific named exact selectors.
*
* @package core
* @category test
* @copyright 2016 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Moodle selectors manager.
*
* @package core
* @copyright 2016 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_exact_named_selector extends \Behat\Mink\Selector\ExactNamedSelector {
// Use the named selector trait.
use behat_named_selector;
/**
* Creates selector instance.
*/
public function __construct() {
$this->registerReplacement('%iconMatch%', "(contains(concat(' ', @class, ' '), ' icon ') or self::img)");
$this->registerReplacement('%imgAltMatch%', './/*[%iconMatch% and (%altMatch% or %titleMatch%)]');
parent::__construct();
}
/**
* @var Allowed types when using text selectors arguments.
*/
protected static $allowedtextselectors = [];
/**
* @var Allowed types when using selector arguments.
*/
protected static $allowedselectors = array(
'button_exact' => 'button',
'checkbox_exact' => 'checkbox',
'field_exact' => 'field',
'fieldset_exact' => 'fieldset',
'link_exact' => 'link',
'link_or_button_exact' => 'link_or_button',
'option_exact' => 'option',
'radio_exact' => 'radio',
'select_exact' => 'select',
'table_exact' => 'table',
'text_exact' => 'text',
);
/** @var List of deprecated selectors */
protected static $deprecatedselectors = [];
/**
* Allowed selectors getter.
*
* @return array
*/
public static function get_allowed_selectors() {
return static::$allowedselectors;
}
/**
* Allowed text selectors getter.
*
* @return array
*/
public static function get_allowed_text_selectors() {
return static::$allowedtextselectors;
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle-specific common functions for named selectors.
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Common functions for named selectors.
*
* This has to be a trait, because we need this in both the classes
* behat_exact_named_selector and behat_partial_named_selector, and
* those classes have to be subclasses of \Behat\Mink\Selector\ExactNamedSelector
* and \Behat\Mink\Selector\PartialNamedSelector. This trait is a way achieve
* that without duplciated code.
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait behat_named_selector {
/**
* Registers new XPath selector with specified name.
*
* @param string $component
* @param behat_component_named_selector $selector
*/
public function register_component_selector(string $component, behat_component_named_selector $selector) {
$alias = $selector->get_alias($component);
$name = $selector->get_name($component);
static::$allowedselectors[$alias] = $name;
if ($selector->is_text_selector()) {
static::$allowedtextselectors[$alias] = $name;
}
// We must use Reflection here. The replacements property is private and cannot be accessed otherwise.
// This is due to an API limitation in Mink.
$rc = new \ReflectionClass(\Behat\Mink\Selector\NamedSelector::class);
$r = $rc->getProperty('replacements');
$replacements = $r->getValue($this);
$selectorxpath = strtr($selector->get_combined_xpath(), $replacements);
parent::registerNamedXpath($name, $selectorxpath);
}
/**
* Registers new XPath selector with specified name.
*
* @param string $component
* @param behat_component_named_replacement $replacement
*/
public function register_replacement(string $component, behat_component_named_replacement $replacement) {
// We must use Reflection here. The replacements property is private and cannot be accessed otherwise.
// This is due to an API limitation in Mink.
$rc = new \ReflectionClass(\Behat\Mink\Selector\NamedSelector::class);
$r = $rc->getProperty('replacements');
$existing = $r->getValue($this);
$from = $replacement->get_from($component);
if (isset($existing[$from])) {
throw new \coding_exception("A named replacement already exists in the partial named selector for '{$from}'. " .
"Replacement names must be unique, and should be namespaced to the component");
}
$translatedto = strtr($replacement->get_to(), $existing);
$this->registerReplacement($from, $translatedto);
}
/**
* Check whether the specified selector has been deprecated and marked for replacement.
*
* @param string $selector
* @return bool
*/
public static function is_deprecated_selector(string $selector): bool {
return array_key_exists($selector, static::$deprecatedselectors);
}
/**
* Fetch the replacement name of a deprecated selector.
*
* @param string $selector
* @return bool
*/
public static function get_deprecated_replacement(string $selector): ?string {
return static::$deprecatedselectors[$selector];
}
}
@@ -0,0 +1,386 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle-specific selectors.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Moodle selectors manager.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSelector {
// Use the named selector trait.
use behat_named_selector;
/**
* Creates selector instance.
*/
public function __construct() {
foreach (self::$customselectors as $alias => $selectors) {
$this->registerNamedXpath($alias, implode(' | ', $selectors));
}
foreach (static::$moodleselectors as $name => $xpath) {
$this->registerNamedXpath($name, $xpath);
}
foreach (self::$customreplacements as $from => $tos) {
$this->registerReplacement($from, implode(' or ', $tos));
}
$this->registerReplacement('%iconMatch%', "(contains(concat(' ', @class, ' '), ' icon ') or self::img)");
$this->registerReplacement('%imgAltMatch%', './/*[%iconMatch% and (%altMatch% or %titleMatch%)]');
parent::__construct();
}
/**
* @var array Allowed types when using text selectors arguments.
*/
protected static $allowedtextselectors = array(
'activity' => 'activity',
'block' => 'block',
'css_element' => 'css_element',
'dialogue' => 'dialogue',
'dropdown_item' => 'dropdown_item',
'fieldset' => 'fieldset',
'icon' => 'icon',
'list_item' => 'list_item',
'question' => 'question',
'region' => 'region',
'section' => 'section',
'table' => 'table',
'table_row' => 'table_row',
'xpath_element' => 'xpath_element',
'form_row' => 'form_row',
'group_message_header' => 'group_message_header',
'group_message' => 'group_message',
'autocomplete' => 'autocomplete',
'iframe' => 'iframe',
'option_role' => 'option_role',
);
/**
* @var array Allowed types when using selector arguments.
*/
protected static $allowedselectors = array(
'activity' => 'activity',
'actionmenu' => 'actionmenu',
'badge' => 'badge',
'block' => 'block',
'button' => 'button',
'checkbox' => 'checkbox',
'combobox' => 'combobox',
'css_element' => 'css_element',
'dialogue' => 'dialogue',
'dropdown' => 'dropdown',
'dropdown_item' => 'dropdown_item',
'field' => 'field',
'fieldset' => 'fieldset',
'file' => 'file',
'filemanager' => 'filemanager',
'group_message' => 'group_message',
'group_message_conversation' => 'group_message_conversation',
'group_message_header' => 'group_message_header',
'group_message_member' => 'group_message_member',
'group_message_tab' => 'group_message_tab',
'group_message_list_area' => 'group_message_list_area',
'group_message_message_content' => 'group_message_message_content',
'heading' => 'heading',
'icon_container' => 'icon_container',
'icon' => 'icon',
'link' => 'link',
'link_or_button' => 'link_or_button',
'list_item' => 'list_item',
'menuitem' => 'menuitem',
'optgroup' => 'optgroup',
'option' => 'option',
'option_role' => 'option_role',
'question' => 'question',
'radio' => 'radio',
'region' => 'region',
'section' => 'section',
'select' => 'select',
'table' => 'table',
'table_row' => 'table_row',
'text' => 'text',
'xpath_element' => 'xpath_element',
'form_row' => 'form_row',
'autocomplete_selection' => 'autocomplete_selection',
'autocomplete_suggestions' => 'autocomplete_suggestions',
'autocomplete' => 'autocomplete',
'iframe' => 'iframe',
);
/**
* Behat by default comes with XPath, CSS and named selectors,
* named selectors are a mapping between names (like button) and
* xpaths that represents that names and includes a placeholder that
* will be replaced by the locator. These are Moodle's own xpaths.
*
* @var array XPaths for moodle elements.
*/
protected static $moodleselectors = array(
'activity' => <<<XPATH
.//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][descendant::*[contains(normalize-space(.), %locator%)]]
XPATH
, 'actionmenu' => <<<XPATH
.//*[
contains(concat(' ', normalize-space(@class), ' '), ' action-menu ')
and
descendant::*[
contains(concat(' ', normalize-space(@class), ' '), ' dropdown-toggle ')
and
(contains(normalize-space(.), %locator%) or descendant::*[%titleMatch%])
]
]
XPATH
, 'badge' => <<<XPATH
.//*[self::span or self::button][(contains(@class, 'badge')) and text()[contains(., %locator%)]]
XPATH
, 'block' => <<<XPATH
.//*[@data-block][contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
descendant::*[self::h2|self::h3|self::h4|self::h5][normalize-space(.) = %locator%] or
@aria-label = %locator%]
XPATH
, 'combobox' => <<<XPATH
.//*[@role='combobox'][%titleMatch% or %ariaLabelMatch% or text()[contains(., %locator%)]]
XPATH
, 'dialogue' => <<<XPATH
.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
not(contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hidden ')) and
normalize-space(descendant::div[
contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hd ')
]) = %locator%] |
.//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
normalize-space(descendant::div[@class='hd']) = %locator%]
|
.//div[@data-region='modal' and descendant::*[@data-region='title'] = %locator%]
|
.//div[
contains(concat(' ', normalize-space(@class), ' '), ' modal-content ')
and
normalize-space(descendant::*[self::h4 or self::h5][contains(concat(' ', normalize-space(@class), ' '), ' modal-title ')]) = %locator%
]
|
.//div[
contains(concat(' ', normalize-space(@class), ' '), ' modal ')
and
normalize-space(descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' modal-header ')]) = %locator%
]
XPATH
, 'dropdown' => <<<XPATH
.//*[
contains(concat(' ', normalize-space(@class), ' '), ' dropdown-menu ')
and
@aria-labelledby =
(//*[
contains(concat(' ', normalize-space(@class), ' '), ' dropdown-toggle ')
and
(contains(normalize-space(.), %locator%) or descendant::*[%titleMatch%])
]/@id)
]
XPATH
, 'dropdown_item' => <<<XPATH
.//*[
@role = 'listitem'
and
(contains(normalize-space(.), %locator%) or descendant::*[%titleMatch%])
]
XPATH
, 'group_message' => <<<XPATH
.//*[@data-conversation-id]//img[contains(@alt, %locator%)]/..
XPATH
, 'group_message_conversation' => <<<XPATH
.//*[@data-region='message-drawer' and contains(., %locator%)]//div[@data-region='content-message-container']
XPATH
, 'group_message_header' => <<<XPATH
.//*[@data-region='message-drawer']//div[@data-region='header-content' and contains(., %locator%)]
XPATH
, 'group_message_member' => <<<XPATH
.//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
//div[@class='list-group' and not(contains(@class, 'hidden'))]//*[text()[contains(., %locator%)]] |
.//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
//div[@data-region='empty-message-container' and not(contains(@class, 'hidden')) and contains(., %locator%)]
XPATH
, 'group_message_tab' => <<<XPATH
.//*[@data-region='message-drawer']//button[@data-toggle='collapse' and contains(string(), %locator%)]
XPATH
, 'group_message_list_area' => <<<XPATH
.//*[@data-region='message-drawer']//*[contains(@data-region, concat('view-overview-', %locator%))]
XPATH
, 'group_message_message_content' => <<<XPATH
.//*[@data-region='message-drawer']//*[@data-region='message' and @data-message-id and contains(., %locator%)]
XPATH
, 'heading' => <<<XPATH
.//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6][contains(normalize-space(.), %locator%)]
XPATH
, 'icon_container' => <<<XPATH
.//span[contains(@data-region, concat(%locator%,'-icon-container'))]
XPATH
, 'icon' => <<<XPATH
.//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
XPATH
, 'list_item' => <<<XPATH
.//li[contains(normalize-space(.), %locator%) and not(.//li[contains(normalize-space(.), %locator%)])]
XPATH
, 'menuitem' => <<<XPATH
.//*[@role='menuitem'][%titleMatch% or %ariaLabelMatch% or text()[contains(., %locator%)]]
XPATH
, 'option_role' => <<<XPATH
.//*[@role='option'][%titleMatch% or %ariaLabelMatch% or text()[contains(., %locator%)]] |
.//*[@role='option']/following-sibling::label[contains(., %locator%)]/preceding-sibling::input
XPATH
, 'question' => <<<XPATH
.//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
[contains(div[@class='content']/div[contains(concat(' ', normalize-space(@class), ' '), ' formulation ')], %locator%)]
XPATH
, 'region' => <<<XPATH
.//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
XPATH
, 'section' => <<<XPATH
.//li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
[normalize-space(.) = %locator%][contains(concat(' ', normalize-space(@class), ' '), ' sectionname ') or
contains(concat(' ', normalize-space(@class), ' '), ' section-title ')]] |
.//div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
[./descendant::*[self::h2][normalize-space(.) = %locator%] or %locator% = 'frontpage']
XPATH
, 'table' => <<<XPATH
.//table[(./@id = %locator% or contains(.//caption, %locator%) or contains(.//th, %locator%) or contains(concat(' ', normalize-space(@class), ' '), %locator% ))]
XPATH
, 'table_row' => <<<XPATH
.//tr[contains(normalize-space(.), %locator%) and not(.//tr[contains(normalize-space(.), %locator%)])]
XPATH
, 'text' => <<<XPATH
.//*[contains(., %locator%) and not(.//*[contains(., %locator%)])]
XPATH
, 'form_row' => <<<XPATH
.//*[contains(concat(' ', @class, ' '), ' col-form-label ')]
[normalize-space(.)= %locator%]
/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]
XPATH
, 'autocomplete_selection' => <<<XPATH
.//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-selection', ' '))]/span[@role='option'][contains(normalize-space(.), %locator%)]
XPATH
, 'autocomplete_suggestions' => <<<XPATH
.//ul[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-suggestions', ' '))]/li[@role='option'][contains(normalize-space(.), %locator%)]
XPATH
, 'autocomplete' => <<<XPATH
.//descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]/ancestor::*[@data-fieldtype = 'autocomplete']
XPATH
, 'iframe' => <<<XPATH
.//iframe[(%idOrNameMatch% or (contains(concat(' ', normalize-space(@class), ' '), %locator% )))]
XPATH
);
protected static $customselectors = [
'field' => [
'upstream' => <<<XPATH
.//*
[%fieldFilterWithPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithPlaceholder%]
|
.//label[%tagTextMatch%]//.//*[%fieldFilterWithPlaceholder%][%notFieldTypeFilter%]
|
.//*
[%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithoutPlaceholder%]
|
.//label[%tagTextMatch%]//.//*[%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%]
XPATH
,
'filemanager' => <<<XPATH
.//*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']
/descendant::input[@id = substring-before(//p[contains(normalize-space(string(.)), %locator%)]/@id, '_label')]
XPATH
,
'passwordunmask' => <<<XPATH
.//*[@data-passwordunmask='wrapper']
/descendant::input[@id = %locator% or @id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
XPATH
,
'inplaceeditable' => <<<XPATH
.//descendant::span[@data-inplaceeditable][descendant::a[%titleMatch%]]
XPATH
,
'date_time' => <<<XPATH
.//fieldset[(%idMatch% or ./legend[%exactTagTextMatch%]) and (@data-fieldtype='date' or @data-fieldtype='date_time')]
XPATH
,
'select_menu' => <<<XPATH
//*[@role='combobox'][@aria-labelledby = //label[contains(normalize-space(string(.)), %locator%)]/@id]
XPATH
,
],
];
/**
* Mink comes with a number of named replacements.
* Sometimes we want to add our own.
*
* @var array XPaths for moodle elements.
*/
protected static $customreplacements = [
'%buttonMatch%' => [
'upstream' => '%idOrNameMatch% or %valueMatch% or %titleMatch%',
'aria' => '%ariaLabelMatch%',
],
'%ariaLabelMatch%' => [
'moodle' => 'contains(./@aria-label, %locator%)',
],
'%exactTagTextMatch%' => [
// This is based upon the upstream tagTextMatch but performs an exact match rather than a loose match using
// contains().
// If possible we should only use exact matches for any new form fields that we add.
'moodle' => 'normalize-space(text())=%locator%',
],
];
/** @var List of deprecated selectors */
protected static $deprecatedselectors = [
'group_message' => 'core_message > Message',
'group_message_member' => 'core_message > Message member',
'group_message_tab' => 'core_message > Message tab',
'group_message_list_area' => 'core_message > Message list area',
'group_message_message_content' => 'core_message > Message content',
];
/**
* Allowed selectors getter.
*
* @return array
*/
public static function get_allowed_selectors() {
return static::$allowedselectors;
}
/**
* Allowed text selectors getter.
*
* @return array
*/
public static function get_allowed_text_selectors() {
return static::$allowedtextselectors;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Editor interface for setting editor values.
*
* @package behat
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_behat;
interface settable_editor {
/**
* Set the editor value.
*
* @param string $editorid The id of the editor within the page
* @param string $value The intended content of the editor
*/
public function set_editor_value(string $editorid, string $value): void;
}
+532
View File
@@ -0,0 +1,532 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Utils for behat-related stuff
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../lib.php');
require_once(__DIR__ . '/../../testing/classes/util.php');
require_once(__DIR__ . '/behat_command.php');
require_once(__DIR__ . '/behat_config_manager.php');
require_once(__DIR__ . '/../../filelib.php');
require_once(__DIR__ . '/../../clilib.php');
require_once(__DIR__ . '/../../csslib.php');
use Behat\Mink\Session;
use Behat\Mink\Exception\ExpectationException;
/**
* Init/reset utilities for Behat database and dataroot
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_util extends testing_util {
/**
* The behat test site fullname and shortname.
*/
const BEHATSITENAME = "Acceptance test site";
/**
* @var array Files to skip when resetting dataroot folder
*/
protected static $datarootskiponreset = array('.', '..', 'behat', 'behattestdir.txt');
/**
* @var array Files to skip when dropping dataroot folder
*/
protected static $datarootskipondrop = array('.', '..', 'lock');
/**
* Installs a site using $CFG->dataroot and $CFG->prefix
* @throws coding_exception
* @return void
*/
public static function install_site() {
global $DB, $CFG;
require_once($CFG->dirroot.'/user/lib.php');
if (!defined('BEHAT_UTIL')) {
throw new coding_exception('This method can be only used by Behat CLI tool');
}
$tables = $DB->get_tables(false);
if (!empty($tables)) {
behat_error(BEHAT_EXITCODE_INSTALLED);
}
// New dataroot.
self::reset_dataroot();
$options = array();
$options['adminuser'] = 'admin';
$options['adminpass'] = 'admin';
$options['fullname'] = self::BEHATSITENAME;
$options['shortname'] = self::BEHATSITENAME;
install_cli_database($options, false);
// We need to keep the installed dataroot filedir files.
// So each time we reset the dataroot before running a test, the default files are still installed.
self::save_original_data_files();
$frontpagesummary = new admin_setting_special_frontpagedesc();
$frontpagesummary->write_setting(self::BEHATSITENAME);
// Update admin user info.
$user = $DB->get_record('user', array('username' => 'admin'));
$user->email = 'moodle@example.com';
$user->firstname = 'Admin';
$user->lastname = 'User';
$user->city = 'Perth';
$user->country = 'AU';
user_update_user($user, false);
// Disable email message processor.
$DB->set_field('message_processors', 'enabled', '0', array('name' => 'email'));
// Sets maximum debug level.
set_config('debug', DEBUG_DEVELOPER);
set_config('debugdisplay', 1);
// Disable some settings that are not wanted on test sites.
set_config('noemailever', 1);
// Enable web cron.
set_config('cronclionly', 0);
// Set editor autosave to high value, so as to avoid unwanted ajax.
set_config('autosavefrequency', '604800', 'editor_atto');
// Set noreplyaddress to an example domain, as it should be valid email address and test site can be a localhost.
set_config('noreplyaddress', 'noreply@example.com');
// Set the support email address.
set_config('supportemail', 'email@example.com');
// Remove any default blocked hosts and port restrictions, to avoid blocking tests (eg those using local files).
set_config('curlsecurityblockedhosts', '');
set_config('curlsecurityallowedport', '');
// Execute all the adhoc tasks.
while ($task = \core\task\manager::get_next_adhoc_task(time())) {
$task->execute();
\core\task\manager::adhoc_task_complete($task);
}
// Keeps the current version of database and dataroot.
self::store_versions_hash();
// Stores the database contents for fast reset.
self::store_database_state();
}
/**
* Build theme CSS.
*/
public static function build_themes($mtraceprogress = false) {
global $CFG;
require_once("{$CFG->libdir}/outputlib.php");
$themenames = array_keys(\core_component::get_plugin_list('theme'));
// Load the theme configs.
$themeconfigs = array_map(function($themename) {
return \theme_config::load($themename);
}, $themenames);
// Build the list of themes and cache them in local cache.
$themes = theme_build_css_for_themes($themeconfigs, ['ltr'], true, $mtraceprogress);
$framework = self::get_framework();
$storageroot = self::get_dataroot() . "/{$framework}/themedata";
foreach ($themes as $themename => $themedata) {
$dirname = "{$storageroot}/{$themename}";
check_dir_exists($dirname);
foreach ($themedata as $direction => $css) {
file_put_contents("{$dirname}/{$direction}.css", $css);
}
}
}
/**
* Drops dataroot and remove test database tables
* @throws coding_exception
* @return void
*/
public static function drop_site() {
if (!defined('BEHAT_UTIL')) {
throw new coding_exception('This method can be only used by Behat CLI tool');
}
self::reset_dataroot();
self::drop_database(true);
self::drop_dataroot();
}
/**
* Delete files and directories under dataroot.
*/
public static function drop_dataroot() {
global $CFG;
// As behat directory is now created under default $CFG->behat_dataroot_parent, so remove the whole dir.
if ($CFG->behat_dataroot !== $CFG->behat_dataroot_parent) {
remove_dir($CFG->behat_dataroot, false);
} else {
// It should never come here.
throw new moodle_exception("Behat dataroot should not be same as parent behat data root.");
}
}
/**
* Checks if $CFG->behat_wwwroot is available and using same versions for cli and web.
*
* @return void
*/
public static function check_server_status() {
global $CFG;
$url = $CFG->behat_wwwroot . '/admin/tool/behat/tests/behat/fixtures/environment.php';
// Get web versions used by behat site.
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
$statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($statuscode !== 200 || empty($result) || (!$result = json_decode($result, true))) {
behat_error (BEHAT_EXITCODE_REQUIREMENT, $CFG->behat_wwwroot . ' is not available, ensure you specified ' .
'correct url and that the server is set up and started.' . PHP_EOL . ' More info in ' .
behat_command::DOCS_URL . PHP_EOL . parent::get_site_info());
}
// Check if cli version is same as web version.
$clienv = self::get_environment();
if ($result != $clienv) {
$output = 'Differences detected between cli and webserver...'.PHP_EOL;
foreach ($result as $key => $version) {
if ($clienv[$key] != $version) {
$output .= ' ' . $key . ': ' . PHP_EOL;
$output .= ' - web server: ' . $version . PHP_EOL;
$output .= ' - cli: ' . $clienv[$key] . PHP_EOL;
}
}
echo $output;
ob_flush();
}
}
/**
* Checks whether the test database and dataroot is ready
* Stops execution if something went wrong
* @throws coding_exception
* @return void
*/
protected static function test_environment_problem() {
global $CFG, $DB;
if (!defined('BEHAT_UTIL')) {
throw new coding_exception('This method can be only used by Behat CLI tool');
}
if (!self::is_test_site()) {
behat_error(1, 'This is not a behat test site!');
}
$tables = $DB->get_tables(false);
if (empty($tables)) {
behat_error(BEHAT_EXITCODE_INSTALL, '');
}
if (!self::is_test_data_updated()) {
behat_error(BEHAT_EXITCODE_REINSTALL, 'The test environment was initialised for a different version');
}
}
/**
* Enables test mode
*
* It uses CFG->behat_dataroot
*
* Starts the test mode checking the composer installation and
* the test environment and updating the available
* features and steps definitions.
*
* Stores a file in dataroot/behat to allow Moodle to switch
* to the test environment when using cli-server.
* @param bool $themesuitewithallfeatures List themes to include core features.
* @param string $tags comma separated tag, which will be given preference while distributing features in parallel run.
* @param int $parallelruns number of parallel runs.
* @param int $run current run.
* @throws coding_exception
* @return void
*/
public static function start_test_mode($themesuitewithallfeatures = false, $tags = '', $parallelruns = 0, $run = 0) {
if (!defined('BEHAT_UTIL')) {
throw new coding_exception('This method can be only used by Behat CLI tool');
}
// Checks the behat set up and the PHP version.
if ($errorcode = behat_command::behat_setup_problem()) {
exit($errorcode);
}
// Check that test environment is correctly set up.
self::test_environment_problem();
// Updates all the Moodle features and steps definitions.
behat_config_manager::update_config_file('', true, $tags, $themesuitewithallfeatures, $parallelruns, $run);
if (self::is_test_mode_enabled()) {
return;
}
$contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
$filepath = self::get_test_file_path();
if (!file_put_contents($filepath, $contents)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
}
}
/**
* Returns the status of the behat test environment
*
* @return int Error code
*/
public static function get_behat_status() {
if (!defined('BEHAT_UTIL')) {
throw new coding_exception('This method can be only used by Behat CLI tool');
}
// Checks the behat set up and the PHP version, returning an error code if something went wrong.
if ($errorcode = behat_command::behat_setup_problem()) {
return $errorcode;
}
// Check that test environment is correctly set up, stops execution.
self::test_environment_problem();
}
/**
* Disables test mode
* @throws coding_exception
* @return void
*/
public static function stop_test_mode() {
if (!defined('BEHAT_UTIL')) {
throw new coding_exception('This method can be only used by Behat CLI tool');
}
$testenvfile = self::get_test_file_path();
behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
if (!self::is_test_mode_enabled()) {
echo "Test environment was already disabled\n";
} else {
if (!unlink($testenvfile)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
}
}
}
/**
* Checks whether test environment is enabled or disabled
*
* To check is the current script is running in the test
* environment
*
* @return bool
*/
public static function is_test_mode_enabled() {
$testenvfile = self::get_test_file_path();
if (file_exists($testenvfile)) {
return true;
}
return false;
}
/**
* Returns the path to the file which specifies if test environment is enabled
* @return string
*/
final public static function get_test_file_path() {
return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
}
/**
* Removes config settings that were added to the main $CFG config within the Behat CLI
* run.
*
* Database storage is already handled by reset_database and existing config values will
* be reset automatically by initialise_cfg(), so we only need to remove added ones.
*/
public static function remove_added_config() {
global $CFG;
if (!empty($CFG->behat_cli_added_config)) {
foreach ($CFG->behat_cli_added_config as $key => $value) {
unset($CFG->{$key});
}
unset($CFG->behat_cli_added_config);
}
}
/**
* Reset contents of all database tables to initial values, reset caches, etc.
*/
public static function reset_all_data() {
// Reset database.
self::reset_database();
// Purge dataroot directory.
self::reset_dataroot();
// Reset all static caches.
accesslib_clear_all_caches(true);
accesslib_reset_role_cache();
// Reset the nasty strings list used during the last test.
nasty_strings::reset_used_strings();
filter_manager::reset_caches();
\core_reportbuilder\manager::reset_caches();
// Reset course and module caches.
core_courseformat\base::reset_course_cache(0);
get_fast_modinfo(0, 0, true);
// Reset the DI container.
\core\di::reset_container();
// Inform data generator.
self::get_data_generator()->reset();
// Reset the task manager.
\core\task\manager::reset_state();
// Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
// $CFG values from the old run. @see set_config.
self::remove_added_config();
initialise_cfg();
}
/**
* Restore theme CSS stored during behat setup.
*/
public static function restore_saved_themes(): void {
global $CFG;
$themerev = theme_get_revision();
$framework = self::get_framework();
$storageroot = self::get_dataroot() . "/{$framework}/themedata";
$themenames = array_keys(\core_component::get_plugin_list('theme'));
$directions = ['ltr', 'rtl'];
$themeconfigs = array_map(function($themename) {
return \theme_config::load($themename);
}, $themenames);
foreach ($themeconfigs as $themeconfig) {
$themename = $themeconfig->name;
$themesubrev = theme_get_sub_revision_for_theme($themename);
$dirname = "{$storageroot}/{$themename}";
foreach ($directions as $direction) {
$cssfile = "{$dirname}/{$direction}.css";
if (file_exists($cssfile)) {
$themeconfig->set_css_content_cache(file_get_contents($cssfile));
}
}
}
}
/**
* Pause execution immediately.
*
* @param Session $session
* @param string $message The message to show when pausing.
* This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
*/
public static function pause(Session $session, string $message): void {
$posixexists = function_exists('posix_isatty');
// Make sure this step is only used with interactive terminal (if detected).
if ($posixexists && !@posix_isatty(STDOUT)) {
throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
}
// Save the cursor position, ring the bell, and add a new line.
fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
// Output the formatted message and reset colour back to normal.
$formattedmessage = cli_ansi_format("{$message}<colour:normal>");
fwrite(STDOUT, $formattedmessage);
// Wait for input.
fread(STDIN, 1024);
// Move the cursor back up to the previous position, then restore the original position stored earlier, and move
// it back down again.
fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
// Add any extra lines back if the provided message was spread over multiple lines.
$linecount = count(explode("\n", $formattedmessage));
fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));
}
/**
* Gets a text-based site version description.
*
* @return string The site info
*/
public static function get_site_info() {
$siteinfo = parent::get_site_info();
$accessibility = empty(behat_config_manager::get_behat_run_config_value('axe')) ? 'No' : 'Yes';
$scssdeprecations = empty(behat_config_manager::get_behat_run_config_value('scss-deprecations')) ? 'No' : 'Yes';
$siteinfo .= <<<EOF
Run optional tests:
- Accessibility: {$accessibility}
- SCSS deprecations: {$scssdeprecations}
EOF;
return $siteinfo;
}
}
+271
View File
@@ -0,0 +1,271 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Files interactions with behat.
*
* Note that steps definitions files can not extend other steps definitions files, so steps definitions which makes use
* of file attachments or filepicker should use this behat_file_helper trait.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_base.php');
use Behat\Mink\Exception\ExpectationException as ExpectationException,
Behat\Mink\Element\NodeElement as NodeElement;
/**
* Files-related actions.
*
* Steps definitions related with filepicker or repositories should extend use this trait as it provides useful methods
* to deal with the common filepicker issues.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait core_behat_file_helper {
/**
* Gets the NodeElement for filepicker of filemanager moodleform element.
*
* The filepicker/filemanager element label is pointing to a hidden input which is
* not recognized as a named selector, as it is hidden...
*
* @throws ExpectationException Thrown by behat_base::find
* @param string $filepickerelement The filepicker form field label
* @return NodeElement The hidden element node.
*/
protected function get_filepicker_node($filepickerelement) {
// More info about the problem (in case there is a problem).
$exception = new ExpectationException('"' . $filepickerelement . '" filepicker can not be found', $this->getSession());
// If no file picker label is mentioned take the first file picker from the page.
if (empty($filepickerelement)) {
$filepickercontainer = $this->find(
'xpath',
"//*[@data-fieldtype=\"filemanager\"]",
$exception
);
} else {
// Gets the ffilemanager node specified by the locator which contains the filepicker container.
$filepickerelement = behat_context_helper::escape($filepickerelement);
$filepickercontainer = $this->find(
'xpath',
"//input[./@id = substring-before(//p[normalize-space(.)=$filepickerelement]/@id, '_label')]" .
"//ancestor::*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']",
$exception
);
}
return $filepickercontainer;
}
/**
* Performs $action on a filemanager container element (file or folder).
*
* It works together with open_element_contextual_menu
* as this method needs the contextual menu to be opened.
*
* @throws ExpectationException Thrown by behat_base::find
* @param string $action
* @param ExpectationException $exception
* @return void
*/
protected function perform_on_element($action, ExpectationException $exception) {
// Finds the button inside the DOM, is a modal window, so should be unique.
$classname = 'fp-file-' . $action;
$button = $this->find('css', '.moodle-dialogue-focused button.' . $classname, $exception);
$this->ensure_node_is_visible($button);
$button->click();
}
/**
* Opens the contextual menu of a folder or a file.
*
* Works both in filemanager elements and when dealing with repository
* elements inside filepicker modal window.
*
* @throws ExpectationException Thrown by behat_base::find
* @param string $name The name of the folder/file
* @param string $filemanagerelement The filemanager form element locator, the repository items are in filepicker modal window if false
* @return void
*/
protected function open_element_contextual_menu($name, $filemanagerelement = false) {
// If a filemanager is specified we restrict the search to the descendants of this particular filemanager form element.
$containernode = false;
$exceptionmsg = '"'.$name.'" element can not be found';
if ($filemanagerelement) {
$containernode = $this->get_filepicker_node($filemanagerelement);
$exceptionmsg = 'The "'.$filemanagerelement.'" filemanager ' . $exceptionmsg;
$locatorprefix = "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]";
} else {
$locatorprefix = "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-items ')]" .
"//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]";
}
$exception = new ExpectationException($exceptionmsg, $this->getSession());
// Avoid quote-related problems.
$name = behat_context_helper::escape($name);
// Get a filepicker/filemanager element (folder or file).
try {
// First we look at the folder as we need to click on the contextual menu otherwise it would be opened.
$node = $this->find(
'xpath',
$locatorprefix .
"//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
"[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" .
"[normalize-space(.)=$name]" .
"//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')]",
$exception,
$containernode
);
} catch (ExpectationException $e) {
// Here the contextual menu is hidden, we click on the thumbnail.
$node = $this->find(
'xpath',
$locatorprefix .
"//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
"[normalize-space(.)=$name]" .
"//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-field ')]",
false,
$containernode
);
}
// Click opens the contextual menu when clicking on files.
$this->ensure_node_is_visible($node);
$node->click();
}
/**
* Opens the filepicker modal window and selects the repository.
*
* @throws ExpectationException Thrown by behat_base::find
* @param NodeElement $filemanagernode The filemanager or filepicker form element DOM node.
* @param mixed $repositoryname The repo name.
* @return void
*/
protected function open_add_file_window($filemanagernode, $repositoryname) {
$exception = new ExpectationException('No files can be added to the specified filemanager', $this->getSession());
// We should deal with single-file and multiple-file filemanagers,
// catching the exception thrown by behat_base::find() in case is not multiple
$this->execute('behat_general::i_click_on_in_the', [
'div.fp-btn-add a, input.fp-btn-choose', 'css_element',
$filemanagernode, 'NodeElement'
]);
// Wait for the default repository (if any) to load. This checks that
// the relevant div exists and that it does not include the loading image.
$this->ensure_element_exists(
"//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]" .
"//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]" .
"[not(descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content-loading ')])]",
'xpath_element');
// Getting the repository link and opening it.
$repoexception = new ExpectationException('The "' . $repositoryname . '" repository has not been found', $this->getSession());
// Avoid problems with both double and single quotes in the same string.
$repositoryname = behat_context_helper::escape($repositoryname);
// Here we don't need to look inside the selected element because there can only be one modal window.
$repositorylink = $this->find(
'xpath',
"//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" .
"//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" .
"[normalize-space(.)=$repositoryname]",
$repoexception
);
// Selecting the repo.
$this->ensure_node_is_visible($repositorylink);
if (!$repositorylink->getParent()->getParent()->hasClass('active')) {
// If the repository link is active, then the repository is already loaded.
// Clicking it while it's active causes issues, so only click it when it isn't (see MDL-51014).
$this->execute('behat_general::i_click_on', [$repositorylink, 'NodeElement']);
}
}
/**
* Waits until the file manager modal windows are closed.
*
* This method is not used by any of our step definitions,
* keeping it here for users already using it.
*
* @throws ExpectationException
* @return void
*/
protected function wait_until_return_to_form() {
$exception = new ExpectationException('The file manager is taking too much time to finish the current action', $this->getSession());
$this->find(
'xpath',
"//div[contains(concat(' ', @class, ' '), ' moodle-dialogue-lightbox ')][contains(@style, 'display: none;')]",
$exception
);
}
/**
* Checks that the file manager contents are not being updated.
*
* This method is not used by any of our step definitions,
* keeping it here for users already using it.
*
* @throws ExpectationException
* @param NodeElement $filepickernode The file manager DOM node
* @return void
*/
protected function wait_until_contents_are_updated($filepickernode) {
$exception = new ExpectationException(
'The file manager contents are requiring too much time to be updated',
$this->getSession()
);
// Looks for the loading image not being displayed. For single-file filepickers is
// only used when accessing the filepicker, there is no filemanager-loading after selecting the file.
$this->find(
'xpath',
"//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager ')]" .
"[not(contains(concat(' ', normalize-space(@class), ' '), ' fm-updating '))]" .
"|" .
"//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager-loading ')]" .
"[contains(@style, 'display: none;')]",
$exception,
$filepickernode
);
}
}
@@ -0,0 +1,80 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context\ContextClass;
use Behat\Behat\Context\ContextClass\ClassResolver as Resolver;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Moodle behat context class resolver.
*
* Resolves arbitrary context strings into a context classes.
*
* @see ContextEnvironmentHandler
*
* @package core
* @copyright 2104 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class ClassResolver implements Resolver {
/** @var array keep list of all behat contexts in moodle. */
private $moodlebehatcontexts = null;
/**
* Constructor for ClassResolver class.
*
* @param array $parameters list of params provided to moodle.
*/
public function __construct($parameters) {
$this->moodlebehatcontexts = $parameters['steps_definitions'];
}
/**
* Checks if resolvers supports provided class.
* Moodle behat context class starts with behat_
*
* @param string $contextstring
* @return Boolean
*/
public function supportsClass($contextstring) {
return (strpos($contextstring, 'behat_') === 0);
}
/**
* Resolves context class.
*
* @param string $contextclass
* @return string context class.
*/
public function resolveClass($contextclass) {
if (!is_array($this->moodlebehatcontexts)) {
throw new \RuntimeException('There are no Moodle context with steps definitions');
}
// Using the key as context identifier load context class.
if (
!empty($this->moodlebehatcontexts[$contextclass]) &&
(file_exists($this->moodlebehatcontexts[$contextclass]))
) {
require_once($this->moodlebehatcontexts[$contextclass]);
} else {
throw new \RuntimeException('Moodle behat context "' . $contextclass . '" not found');
}
return $contextclass;
}
}
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context\Initializer;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\Initializer\ContextInitializer;
use Moodle\BehatExtension\Context\MoodleContext;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* MoodleContext initializer
*
* @package core
* @author David Monllaó <david.monllao@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleAwareInitializer implements ContextInitializer {
/** @var array The list of parameters */
private $parameters;
/**
* Initializes initializer.
*
* @param array $parameters
*/
public function __construct(array $parameters) {
$this->parameters = $parameters;
}
/**
* Initializes provided context.
*
* @param Context $context
*/
public function initializeContext(Context $context) {
if ($context instanceof MoodleContext) {
$context->setMoodleConfig($this->parameters);
}
}
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context;
use Behat\MinkExtension\Context\RawMinkContext;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Moodle contexts loader
*
* It gathers all the available steps definitions reading the
* Moodle configuration file
*
* @package core
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleContext extends RawMinkContext {
/** @var array Moodle features and steps definitions list */
protected $moodleconfig;
/**
* Includes all the specified Moodle subcontexts.
*
* @param array $parameters
*/
public function setMoodleConfig(array $parameters): void {
$this->moodleconfig = $parameters;
}
}
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context\Step;
use Behat\Gherkin\Node\StepNode;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Chained Step base class.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class ChainedStep extends StepNode {
/**
* @var string
*/
private $language;
// phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
/**
* Initializes ChainedStep.
*
* @param string $keyword
* @param string $text
* @param array $arguments
* @param int $line
* @param string $keywordtype
*/
public function __construct($keyword, $text, array $arguments, $line = 0, $keywordtype = 'Given') {
parent::__construct($keyword, $text, $arguments, $line, $keywordtype);
}
// phpcs:enable Generic.CodeAnalysis.UselessOverridingMethod.Found
/**
* Sets language.
*
* @param string $language
*/
public function setLanguage($language) {
$this->language = $language;
}
/**
* Returns language.
*
* @return string
*/
public function getLanguage() {
return $this->language;
}
}
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context\Step;
/**
* Chained `Given` step.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class Given extends ChainedStep {
/**
* Initializes `Given` sub-step.
*/
public function __construct() {
$arguments = func_get_args();
$text = array_shift($arguments);
parent::__construct('Given', $text, $arguments);
}
}
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context\Step;
/**
* Chained `Then` ChainedStep.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class Then extends ChainedStep {
/**
* Initializes `Then` sub-step.
*/
public function __construct() {
$arguments = func_get_args();
$text = array_shift($arguments);
parent::__construct('Then', $text, $arguments);
}
}
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Context\Step;
/**
* Chained `When` step.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class When extends ChainedStep {
/**
* Initializes `When` sub-step.
*/
public function __construct() {
$arguments = func_get_args();
$text = array_shift($arguments);
parent::__construct('When', $text, $arguments);
}
}
@@ -0,0 +1,123 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Definition\Cli;
use Behat\Behat\Definition\DefinitionWriter;
use Behat\Behat\Definition\Printer\ConsoleDefinitionListPrinter;
use Behat\Testwork\Cli\Controller;
use Behat\Testwork\Suite\SuiteRepository;
use Moodle\BehatExtension\Definition\Printer\ConsoleDefinitionInformationPrinter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Available definition controller, for calling moodle information printer.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class AvailableDefinitionsController implements Controller {
/** @var SuiteRepository */
private $suiterepository;
/** @var DefinitionWriter */
private $writer;
/** @var ConsoleDefinitionListPrinter */
private $listprinter;
/** @var ConsoleDefinitionInformationPrinter */
private $infoprinter;
/**
* Initializes controller.
*
* @param SuiteRepository $suiterepository
* @param DefinitionWriter $writer
* @param ConsoleDefinitionListPrinter $listprinter
* @param ConsoleDefinitionInformationPrinter $infoprinter
*/
public function __construct(
SuiteRepository $suiterepository,
DefinitionWriter $writer,
ConsoleDefinitionListPrinter $listprinter,
ConsoleDefinitionInformationPrinter $infoprinter
) {
$this->suiterepository = $suiterepository;
$this->writer = $writer;
$this->listprinter = $listprinter;
$this->infoprinter = $infoprinter;
}
/**
* Configures command to be executable by the controller.
*
* @param Command $command
*/
public function configure(Command $command) {
$command->addOption('--definitions', '-d', InputOption::VALUE_REQUIRED,
"Print all available step definitions:" . PHP_EOL .
"- use <info>--definitions l</info> to just list definition expressions." . PHP_EOL .
"- use <info>--definitions i</info> to show definitions with extended info." . PHP_EOL .
"- use <info>--definitions 'needle'</info> to find specific definitions." . PHP_EOL .
"Use <info>--lang</info> to see definitions in specific language."
);
}
/**
* Executes controller.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return null|integer
*/
public function execute(InputInterface $input, OutputInterface $output) {
if (null === $argument = $input->getOption('definitions')) {
return null;
}
$printer = $this->getdefinitionPrinter($argument);
foreach ($this->suiterepository->getSuites() as $suite) {
$this->writer->printSuiteDefinitions($printer, $suite);
}
return 0;
}
/**
* Returns definition printer for provided option argument.
*
* @param string $argument
*
* @return \Behat\Behat\Definition\Printer\DefinitionPrinter
*/
private function getdefinitionprinter($argument) {
if ('l' === $argument) {
return $this->listprinter;
}
if ('i' !== $argument) {
$this->infoprinter->setSearchCriterion($argument);
}
return $this->infoprinter;
}
}
@@ -0,0 +1,105 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Definition\Printer;
use Behat\Behat\Definition\Printer\ConsoleDefinitionPrinter;
use Behat\Testwork\Suite\Suite;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Moodle console definition information printer.
*
* Used in moodle for definition printing.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class ConsoleDefinitionInformationPrinter extends ConsoleDefinitionPrinter {
/** @var null|string */
private $searchcriterion;
/**
* Sets search criterion.
*
* @param string $criterion
*/
public function setSearchCriterion($criterion) {
$this->searchcriterion = $criterion;
}
/**
* Prints definition.
*
* @param Suite $suite
* @param Definition[] $definitions
*/
public function printDefinitions(Suite $suite, $definitions) {
$template = <<<TPL
<div class="step"><div class="stepdescription">{description}</div>
<div class="stepcontent"><span class="steptype">{type}</span><span class="stepregex">{regex}</span></div>
<div class="stepapipath">{apipath}</div>
</div>
TPL;
$search = $this->searchcriterion;
// If there is a specific type (given, when or then) required.
if ($search && strpos($search, '&&') !== false) {
list($search, $type) = explode('&&', $search);
}
foreach ($definitions as $definition) {
$definition = $this->translateDefinition($suite, $definition);
if (!empty($type) && strtolower($definition->getType()) != $type) {
continue;
}
$pattern = $definition->getPattern();
if ($search && !preg_match('/' . str_replace(' ', '.*', preg_quote($search, '/') . '/'), $pattern)) {
continue;
}
$description = $definition->getDescription();
// Removing beginning and end.
$pattern = substr($pattern, 2, strlen($pattern) - 4);
// Replacing inline regex for expected info string.
$pattern = preg_replace_callback(
'/"\(\?P<([^>]*)>(.*?)"( |$)/',
function ($matches) {
return '"' . strtoupper($matches[1]) . '" ';
},
$pattern
);
$definitiontoprint[] = strtr($template, [
'{regex}' => $pattern,
'{type}' => str_pad($definition->getType(), 5, ' ', STR_PAD_LEFT),
'{description}' => $description ? $description : '',
'{apipath}' => $definition->getPath()
]);
$this->write(implode("\n", $definitiontoprint));
unset($definitiontoprint);
}
}
}
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Driver;
use Behat\Mink\Exception\DriverException;
use OAndreyev\Mink\Driver\WebDriver as UpstreamDriver;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* WebDriver Driver to allow extra selenium capabilities required by Moodle.
*
* @package core
* @copyright 2016 onwards Rajesh Taneja
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class WebDriver extends UpstreamDriver {
/**
* Dirty attribute to get the browser name; $browserName is private
* @var string
*/
protected static $browser;
/**
* Instantiates the driver.
*
* @param string $browsername Browser name
* @param array $desiredcapabilities The desired capabilities
* @param string $wdhost The WebDriver host
* @param array $moodleparameters Moodle parameters including our non-behat-friendly selenium capabilities
*/
public function __construct(
$browsername = 'chrome',
$desiredcapabilities = null,
$wdhost = 'http://localhost:4444/wd/hub',
$moodleparameters = []
) {
parent::__construct($browsername, $desiredcapabilities, $wdhost);
// This class is instantiated by the dependencies injection system so prior to all of beforeSuite subscribers
// which will call getBrowser*().
self::$browser = $browsername;
}
/**
* Returns the browser being used.
*
* We need to know it:
* - To show info about the run.
* - In case there are differences between browsers in the steps.
*
* @return string
*/
public static function getBrowserName() {
return self::$browser;
}
/**
* Post key on specified xpath.
*
* @param string $key
* @param string $xpath
*/
public function post_key($key, $xpath) {
throw new \Exception('No longer used - please use keyDown and keyUp');
}
#[\Override]
public function stop(): void {
try {
parent::stop();
} catch (DriverException $e) {
error_log($e->getMessage());
$rcp = new \ReflectionProperty(parent::class, 'webDriver');
$rcp->setValue($this, null);
}
}
}
@@ -0,0 +1,91 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Driver;
use Behat\MinkExtension\ServiceContainer\Driver\DriverFactory;
use OAndreyev\Mink\Driver\WebDriverFactory as UpstreamFactory;
use Symfony\Component\DependencyInjection\Definition;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Driver factory for the Moodle WebDriver.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class WebDriverFactory extends UpstreamFactory implements DriverFactory {
/**
* Builds the service definition for the driver.
*
* @param array $config
* @return Definition
*/
public function buildDriver(array $config) {
// Merge capabilities.
$extracapabilities = $config['capabilities']['extra_capabilities'];
unset($config['capabilities']['extra_capabilities']);
// Normalise the Edge browser name.
if ($config['browser'] === 'edge') {
$config['browser'] = 'MicrosoftEdge';
}
// Ensure that the capabilites.browserName is set correctly.
$config['capabilities']['browserName'] = $config['browser'];
$capabilities = array_replace($extracapabilities, $config['capabilities']);
// Incorrect top level capabilities lead to invalid Selenium browser selection.
// See https://github.com/SeleniumHQ/selenium/issues/10410 for more information.
// If any of these settings are mentioned then additional empty Capability options are created and a random
// browser is chosen.
$filteredcapabilities = [
'tags',
'ignoreZoomSetting',
'marionette',
'browser',
'name',
];
foreach ($filteredcapabilities as $capabilityname) {
unset($capabilities[$capabilityname]);
}
// Build driver definition.
return new Definition(WebDriver::class, [
$config['browser'],
$capabilities,
$config['wd_host'],
]);
}
/**
* Get the CapabilitiesNode.
*
* @return Node
*/
protected function getCapabilitiesNode() {
$node = parent::getCapabilitiesNode();
// Specify chrome as the default browser.
$node->find('browser')->defaultValue('chrome');
return $node;
}
}
@@ -0,0 +1,280 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\EventDispatcher\Tester;
use Behat\Behat\EventDispatcher\Event\AfterStepSetup;
use Behat\Behat\EventDispatcher\Event\AfterStepTested;
use Behat\Behat\EventDispatcher\Event\BeforeStepTeardown;
use Behat\Behat\EventDispatcher\Event\BeforeStepTested;
use Behat\Behat\Tester\Result\ExecutedStepResult;
use Behat\Behat\Tester\Result\SkippedStepResult;
use Behat\Behat\Tester\Result\StepResult;
use Behat\Behat\Tester\Result\UndefinedStepResult;
use Behat\Behat\Tester\StepTester;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Testwork\Call\CallResult;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\TestworkEventDispatcher;
use Moodle\BehatExtension\Context\Step\ChainedStep;
use Moodle\BehatExtension\Exception\SkippedException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Override step tester to ensure chained steps gets executed.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ChainedStepTester implements StepTester {
/**
* The text of the step to look for exceptions / debugging messages.
*/
const EXCEPTIONS_STEP_TEXT = 'I look for exceptions';
/**
* @var StepTester Base step tester.
*/
private $singlesteptester;
/**
* @var EventDispatcher keep step event dispatcher.
*/
private $eventdispatcher;
/**
* Keep status of chained steps if used.
* @var bool
*/
protected static $chainedstepused = false;
/**
* Constructor.
*
* @param StepTester $steptester single step tester.
*/
public function __construct(StepTester $steptester) {
$this->singlesteptester = $steptester;
}
/**
* Set event dispatcher to use for events.
*
* @param EventDispatcherInterface $eventdispatcher
*/
public function setEventDispatcher(EventDispatcherInterface $eventdispatcher) {
$this->eventdispatcher = $eventdispatcher;
}
/**
* Sets up step for a test.
*
* @param Environment $env
* @param FeatureNode $feature
* @param StepNode $step
* @param bool $skip
*
* @return Setup
*/
public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip) {
return $this->singlesteptester->setUp($env, $feature, $step, $skip);
}
/**
* Tests step.
*
* @param Environment $env
* @param FeatureNode $feature
* @param StepNode $step
* @param bool $skip
* @return StepResult
*/
public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip) {
$result = $this->singlesteptester->test($env, $feature, $step, $skip);
if (!($result instanceof ExecutedStepResult) || !$this->supportsResult($result->getCallResult())) {
$result = $this->checkSkipResult($result);
// If undefined step then don't continue chained steps.
if ($result instanceof UndefinedStepResult) {
return $result;
}
// If exception caught, then don't continue chained steps.
if (($result instanceof ExecutedStepResult) && $result->hasException()) {
return $result;
}
// If step is skipped, then return. no need to continue chain steps.
if ($result instanceof SkippedStepResult) {
return $result;
}
// Check for exceptions.
// Extra step, looking for a moodle exception, a debugging() message or a PHP debug message.
$checkingstep = new StepNode('Given', self::EXCEPTIONS_STEP_TEXT, [], $step->getLine());
$afterexceptioncheckingevent = $this->singlesteptester->test($env, $feature, $checkingstep, $skip);
$exceptioncheckresult = $this->checkSkipResult($afterexceptioncheckingevent);
if (!$exceptioncheckresult->isPassed()) {
return $exceptioncheckresult;
}
return $result;
}
return $this->runChainedSteps($env, $feature, $result, $skip);
}
/**
* Tears down step after a test.
*
* @param Environment $env
* @param FeatureNode $feature
* @param StepNode $step
* @param bool $skip
* @param StepResult $result
* @return Teardown
*/
public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result) {
return $this->singlesteptester->tearDown($env, $feature, $step, $skip, $result);
}
/**
* Check if results supported.
*
* @param CallResult $result
* @return bool
*/
private function supportsResult(CallResult $result) {
$return = $result->getReturn();
if ($return instanceof ChainedStep) {
return true;
}
if (!is_array($return) || empty($return)) {
return false;
}
foreach ($return as $value) {
if (!$value instanceof ChainedStep) {
return false;
}
}
return true;
}
/**
* Run chained steps.
*
* @param Environment $env
* @param FeatureNode $feature
* @param ExecutedStepResult $result
* @param bool $skip
* @return ExecutedStepResult|StepResult
*/
private function runChainedSteps(Environment $env, FeatureNode $feature, ExecutedStepResult $result, $skip) {
// Set chained setp is used, so it can be used by formatter to o/p.
self::$chainedstepused = true;
$callresult = $result->getCallResult();
$steps = $callresult->getReturn();
if (!is_array($steps)) {
// Test it, no need to dispatch events for single chain.
$stepresult = $this->test($env, $feature, $steps, $skip);
return $this->checkSkipResult($stepresult);
}
// Test all steps.
foreach ($steps as $step) {
// Setup new step.
$event = new BeforeStepTested($env, $feature, $step);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::BEFORE);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::BEFORE, $event);
}
$setup = $this->setUp($env, $feature, $step, $skip);
$event = new AfterStepSetup($env, $feature, $step, $setup);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::AFTER_SETUP);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::AFTER_SETUP, $event);
}
// Test it.
$stepresult = $this->test($env, $feature, $step, $skip);
// Tear down.
$event = new BeforeStepTeardown($env, $feature, $step, $result);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::BEFORE_TEARDOWN);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::BEFORE_TEARDOWN, $event);
}
$teardown = $this->tearDown($env, $feature, $step, $skip, $result);
$event = new AfterStepTested($env, $feature, $step, $result, $teardown);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::AFTER);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::AFTER, $event);
}
if (!$stepresult->isPassed()) {
return $this->checkSkipResult($stepresult);
}
}
return $this->checkSkipResult($stepresult);
}
/**
* Handle skip exception.
*
* @param StepResult $result
*
* @return ExecutedStepResult|SkippedStepResult
*/
private function checkSkipResult(StepResult $result) {
if ((method_exists($result, 'getException')) && ($result->getException() instanceof SkippedException)) {
return new SkippedStepResult($result->getSearchResult());
} else {
return $result;
}
}
/**
* Returns if cahined steps are used.
* @return bool.
*/
public static function is_chained_step_used() {
return self::$chainedstepused;
}
}
@@ -0,0 +1,140 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\EventDispatcher\Tester;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
use Behat\Behat\EventDispatcher\Event\AfterStepSetup;
use Behat\Behat\EventDispatcher\Event\AfterStepTested;
use Behat\Behat\EventDispatcher\Event\BeforeStepTeardown;
use Behat\Behat\EventDispatcher\Event\BeforeStepTested;
use Behat\Behat\Tester\Result\StepResult;
use Behat\Behat\Tester\StepTester;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\TestworkEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Step tester dispatching BEFORE/AFTER events during tests.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class MoodleEventDispatchingStepTester implements StepTester {
/** @var StepTester */
private $basetester;
/** @var EventDispatcherInterface */
private $eventdispatcher;
/**
* Initializes tester.
*
* @param StepTester $basetester
* @param EventDispatcherInterface $eventdispatcher
*/
public function __construct(StepTester $basetester, EventDispatcherInterface $eventdispatcher) {
$this->basetester = $basetester;
$this->eventdispatcher = $eventdispatcher;
}
/**
* Sets up step for a test.
*
* @param Environment $env
* @param FeatureNode $feature
* @param StepNode $step
* @param bool $skip
*
* @return Setup
*/
public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip) {
$event = new BeforeStepTested($env, $feature, $step);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::BEFORE);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::BEFORE, $event);
}
$setup = $this->basetester->setUp($env, $feature, $step, $skip);
$this->basetester->setEventDispatcher($this->eventdispatcher);
$event = new AfterStepSetup($env, $feature, $step, $setup);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::AFTER_SETUP);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::AFTER_SETUP, $event);
}
return $setup;
}
/**
* Tests step.
*
* @param Environment $env
* @param FeatureNode $feature
* @param StepNode $step
* @param bool $skip
* @return StepResult
*/
public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip) {
return $this->basetester->test($env, $feature, $step, $skip);
}
/**
* Tears down step after a test.
*
* @param Environment $env
* @param FeatureNode $feature
* @param StepNode $step
* @param bool $skip
* @param StepResult $result
* @return Teardown
*/
public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result) {
$event = new BeforeStepTeardown($env, $feature, $step, $result);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::BEFORE_TEARDOWN);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::BEFORE_TEARDOWN, $event);
}
$teardown = $this->basetester->tearDown($env, $feature, $step, $skip, $result);
$event = new AfterStepTested($env, $feature, $step, $result, $teardown);
if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
// Symfony 4.3 and up.
$this->eventdispatcher->dispatch($event, $event::AFTER);
} else {
// TODO: Remove when our min supported version is >= 4.3.
$this->eventdispatcher->dispatch($event::AFTER, $event);
}
return $teardown;
}
}
@@ -0,0 +1,26 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Exception;
/**
* Skipped exception (throw this to mark step as "skipped").
*
* @package core
* @author Jerome Mouneyrac
*/
class SkippedException extends \Exception {
}
@@ -0,0 +1,147 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Output\Formatter;
use Behat\Behat\EventDispatcher\Event\AfterOutlineTested;
use Behat\Behat\EventDispatcher\Event\AfterScenarioTested;
use Behat\Testwork\Output\Formatter;
use Behat\Testwork\Output\Printer\OutputPrinter;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Feature step counter for distributing features between parallel runs.
*
* Use it with --dry-run (and any other selectors combination) to
* get the results quickly.
*
* @package core
* @copyright 2015 onwards Rajesh Taneja
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleListFormatter implements Formatter {
/** @var OutputPrinter */
private $printer;
/** @var array */
private $parameters;
/** @var string */
private $name;
/** @var string */
private $description;
/**
* Initializes formatter.
*
* @param string $name
* @param string $description
* @param array $parameters
* @param OutputPrinter $printer
*/
public function __construct($name, $description, array $parameters, OutputPrinter $printer) {
$this->name = $name;
$this->description = $description;
$this->parameters = $parameters;
$this->printer = $printer;
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* @return array The event names to listen to
*/
public static function getSubscribedEvents() {
return [
'tester.scenario_tested.after' => 'afterScenario',
'tester.outline_tested.after' => 'afterOutlineExample',
];
}
/**
* Returns formatter name.
*
* @return string
*/
public function getName() {
return $this->name;
}
/**
* Returns formatter description.
*
* @return string
*/
public function getDescription() {
return $this->description;
}
/**
* Returns formatter output printer.
*
* @return OutputPrinter
*/
public function getOutputPrinter() {
return $this->printer;
}
/**
* Sets formatter parameter.
*
* @param string $name
* @param mixed $value
*/
public function setParameter($name, $value) {
$this->parameters[$name] = $value;
}
/**
* Returns parameter name.
*
* @param string $name
*
* @return mixed
*/
public function getParameter($name) {
return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
}
/**
* Listens to "scenario.after" event.
*
* @param AfterScenarioTested $event
*/
public function afterScenario(AfterScenarioTested $event) {
$scenario = $event->getScenario();
$this->printer->writeln($event->getFeature()->getFile() . ':' . $scenario->getLine());
}
/**
* Listens to "outline.example.after" event.
*
* @param AfterOutlineTested $event
*/
public function afterOutlineExample(AfterOutlineTested $event) {
$outline = $event->getOutline();
$line = $outline->getLine();
$this->printer->writeln($event->getFeature()->getFile() . ':' . $line);
}
}
@@ -0,0 +1,210 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Output\Formatter;
use Behat\Testwork\Exception\ServiceContainer\ExceptionExtension;
use Behat\Testwork\Output\ServiceContainer\Formatter\FormatterFactory;
use Behat\Testwork\Output\ServiceContainer\OutputExtension;
use Behat\Testwork\ServiceContainer\ServiceProcessor;
use Behat\Testwork\Translator\ServiceContainer\TranslatorExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Moodle behat context class resolver.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleProgressFormatterFactory implements FormatterFactory {
/** @var ServiceProcessor */
private $processor;
/** @var string moodle progress ID */
const ROOT_LISTENER_ID_MOODLE = 'output.node.listener.moodleprogress';
/** @var string moodle printer ID */
const RESULT_TO_STRING_CONVERTER_ID_MOODLE = 'output.node.printer.result_to_string';
/** @var string Available extension points */
const ROOT_LISTENER_WRAPPER_TAG_MOODLE = 'output.node.listener.moodleprogress.wrapper';
/**
* Initializes extension.
*
* @param null|ServiceProcessor $processor
*/
public function __construct(ServiceProcessor $processor = null) {
$this->processor = $processor ? : new ServiceProcessor();
}
/**
* Builds formatter configuration.
*
* @param ContainerBuilder $container
*/
public function buildFormatter(ContainerBuilder $container) {
$this->loadRootNodeListener($container);
$this->loadCorePrinters($container);
$this->loadPrinterHelpers($container);
$this->loadFormatter($container);
}
/**
* Processes formatter configuration.
*
* @param ContainerBuilder $container
*/
public function processFormatter(ContainerBuilder $container) {
$this->processListenerWrappers($container);
}
/**
* Loads progress formatter node event listener.
*
* @param ContainerBuilder $container
*/
protected function loadRootNodeListener(ContainerBuilder $container) {
$definition = new Definition('Behat\Behat\Output\Node\EventListener\AST\StepListener', [
new Reference('output.node.printer.moodleprogress.step')
]);
$container->setDefinition(self::ROOT_LISTENER_ID_MOODLE, $definition);
}
/**
* Loads formatter itself.
*
* @param ContainerBuilder $container
*/
protected function loadFormatter(ContainerBuilder $container) {
$definition = new Definition('Behat\Behat\Output\Statistics\TotalStatistics');
$container->setDefinition('output.moodleprogress.statistics', $definition);
$moodleconfig = $container->getParameter('behat.moodle.parameters');
$definition = new Definition(
'Moodle\BehatExtension\Output\Printer\MoodleProgressPrinter',
[$moodleconfig['moodledirroot']]
);
$container->setDefinition('moodle.output.node.printer.moodleprogress.printer', $definition);
$definition = new Definition('Behat\Testwork\Output\NodeEventListeningFormatter', [
'moodle_progress',
'Prints information about then run followed by one character per step.',
[
'timer' => true
],
$this->createOutputPrinterDefinition(),
new Definition('Behat\Testwork\Output\Node\EventListener\ChainEventListener', [
[
new Reference(self::ROOT_LISTENER_ID_MOODLE),
new Definition('Behat\Behat\Output\Node\EventListener\Statistics\StatisticsListener', [
new Reference('output.moodleprogress.statistics'),
new Reference('output.node.printer.moodleprogress.statistics')
]),
new Definition('Behat\Behat\Output\Node\EventListener\Statistics\ScenarioStatsListener', [
new Reference('output.moodleprogress.statistics')
]),
new Definition('Behat\Behat\Output\Node\EventListener\Statistics\StepStatsListener', [
new Reference('output.moodleprogress.statistics'),
new Reference(ExceptionExtension::PRESENTER_ID)
]),
new Definition('Behat\Behat\Output\Node\EventListener\Statistics\HookStatsListener', [
new Reference('output.moodleprogress.statistics'),
new Reference(ExceptionExtension::PRESENTER_ID)
]),
new Definition('Behat\Behat\Output\Node\EventListener\AST\SuiteListener', [
new Reference('moodle.output.node.printer.moodleprogress.printer')
])
]
])
]);
$definition->addTag(OutputExtension::FORMATTER_TAG, ['priority' => 1]);
$container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodleprogress', $definition);
}
/**
* Loads printer helpers.
*
* @param ContainerBuilder $container
*/
protected function loadPrinterHelpers(ContainerBuilder $container) {
$definition = new Definition('Behat\Behat\Output\Node\Printer\Helper\ResultToStringConverter');
$container->setDefinition(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE, $definition);
}
/**
* Loads feature, scenario and step printers.
*
* @param ContainerBuilder $container
*/
protected function loadCorePrinters(ContainerBuilder $container) {
$definition = new Definition('Behat\Behat\Output\Node\Printer\CounterPrinter', [
new Reference(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE),
new Reference(TranslatorExtension::TRANSLATOR_ID),
]);
$container->setDefinition('output.node.moodle.printer.counter', $definition);
$definition = new Definition('Behat\Behat\Output\Node\Printer\ListPrinter', [
new Reference(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE),
new Reference(ExceptionExtension::PRESENTER_ID),
new Reference(TranslatorExtension::TRANSLATOR_ID),
'%paths.base%'
]);
$container->setDefinition('output.node.moodle.printer.list', $definition);
$definition = new Definition('Behat\Behat\Output\Node\Printer\Progress\ProgressStepPrinter', [
new Reference(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE)
]);
$container->setDefinition('output.node.printer.moodleprogress.step', $definition);
$definition = new Definition('Behat\Behat\Output\Node\Printer\Progress\ProgressStatisticsPrinter', [
new Reference('output.node.moodle.printer.counter'),
new Reference('output.node.moodle.printer.list')
]);
$container->setDefinition('output.node.printer.moodleprogress.statistics', $definition);
}
/**
* Creates output printer definition.
*
* @return Definition
*/
protected function createOutputPrinterDefinition() {
return new Definition('Behat\Testwork\Output\Printer\StreamOutputPrinter', [
new Definition('Behat\Behat\Output\Printer\ConsoleOutputFactory'),
]);
}
/**
* Processes all registered pretty formatter node listener wrappers.
*
* @param ContainerBuilder $container
*/
protected function processListenerWrappers(ContainerBuilder $container) {
$this->processor->processWrapperServices(
$container,
self::ROOT_LISTENER_ID_MOODLE,
self::ROOT_LISTENER_WRAPPER_TAG_MOODLE
);
}
}
@@ -0,0 +1,295 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Output\Formatter;
use Behat\Behat\EventDispatcher\Event\AfterStepTested;
use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested;
use Behat\Behat\EventDispatcher\Event\BeforeStepTested;
use Behat\Testwork\Output\Formatter;
use Behat\Testwork\Output\Printer\OutputPrinter;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Feature step counter for distributing features between parallel runs.
*
* Use it with --dry-run (and any other selectors combination) to
* get the results quickly.
*
* @package core
* @copyright 2016 onwards Rajesh Taneja
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleScreenshotFormatter implements Formatter {
/** @var OutputPrinter */
private $printer;
/** @var array */
private $parameters;
/** @var string */
private $name;
/** @var string */
private $description;
/** @var int The scenario count */
protected static $currentscenariocount = 0;
/** @var int The step count within the current scenario */
protected static $currentscenariostepcount = 0;
/**
* If we are saving any kind of dump on failure we should use the same parent dir during a run.
*
* @var The parent dir name
*/
protected static $faildumpdirname = false;
/**
* Initializes formatter.
*
* @param string $name
* @param string $description
* @param array $parameters
* @param OutputPrinter $printer
*/
public function __construct($name, $description, array $parameters, OutputPrinter $printer) {
$this->name = $name;
$this->description = $description;
$this->parameters = $parameters;
$this->printer = $printer;
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* @return array The event names to listen to
*/
public static function getSubscribedEvents() {
return [
'tester.scenario_tested.before' => 'beforeScenario',
'tester.step_tested.before' => 'beforeStep',
'tester.step_tested.after' => 'afterStep',
];
}
/**
* Returns formatter name.
*
* @return string
*/
public function getName() {
return $this->name;
}
/**
* Returns formatter description.
*
* @return string
*/
public function getDescription() {
return $this->description;
}
/**
* Returns formatter output printer.
*
* @return OutputPrinter
*/
public function getOutputPrinter() {
return $this->printer;
}
/**
* Sets formatter parameter.
*
* @param string $name
* @param mixed $value
*/
public function setParameter($name, $value) {
$this->parameters[$name] = $value;
}
/**
* Returns parameter name.
*
* @param string $name
* @return mixed
*/
public function getParameter($name) {
return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
}
/**
* Reset currentscenariostepcount
*
* @param BeforeScenarioTested $event
*/
public function beforeScenario(BeforeScenarioTested $event) {
self::$currentscenariostepcount = 0;
self::$currentscenariocount++;
}
/**
* Increment currentscenariostepcount
*
* @param BeforeStepTested $event
*/
public function beforeStep(BeforeStepTested $event) {
self::$currentscenariostepcount++;
}
/**
* Take screenshot after step is executed. Behat\Behat\Event\html
*
* @param AfterStepTested $event
*/
public function afterStep(AfterStepTested $event) {
$behathookcontext = $event->getEnvironment()->getContext('behat_hooks');
$formats = $this->getParameter('formats');
$formats = explode(',', $formats);
// Take screenshot.
if (in_array('image', $formats)) {
$this->take_screenshot($event, $behathookcontext);
}
// Save html content.
if (in_array('html', $formats)) {
$this->take_contentdump($event, $behathookcontext);
}
}
/**
* Return screenshot directory where all screenshots will be saved.
*
* @return string
*/
protected function get_run_screenshot_dir() {
global $CFG;
if (self::$faildumpdirname) {
return self::$faildumpdirname;
}
// If output_path is set then use output_path else use faildump_path.
if ($this->getOutputPrinter()->getOutputPath()) {
$screenshotpath = $this->getOutputPrinter()->getOutputPath();
} else if ($CFG->behat_faildump_path) {
$screenshotpath = $CFG->behat_faildump_path;
} else {
// It should never reach here.
throw new FormatterException('You should specify --out "SOME/PATH" for moodle_screenshot format');
}
if ($this->getParameter('dir_permissions')) {
$dirpermissions = $this->getParameter('dir_permissions');
} else {
$dirpermissions = 0777;
}
// All the screenshot dumps should be in the same parent dir.
self::$faildumpdirname = $screenshotpath . DIRECTORY_SEPARATOR . date('Ymd_His');
if (!is_dir(self::$faildumpdirname) && !mkdir(self::$faildumpdirname, $dirpermissions, true)) {
// It shouldn't, we already checked that the directory is writable.
throw new FormatterException(sprintf(
'No directories can be created inside %s, check the directory permissions.', $screenshotpath
));
}
return self::$faildumpdirname;
}
/**
* Take screenshot when a step fails.
*
* @throws Exception
* @param AfterStepTested $event
* @param Context $context
*/
protected function take_screenshot(AfterStepTested $event, $context) {
// BrowserKit can't save screenshots.
if ($context->getMink()->isSessionStarted($context->getMink()->getDefaultSessionName())) {
if (get_class($context->getMink()->getSession()->getDriver()) === 'Behat\Mink\Driver\BrowserKitDriver') {
return false;
}
list ($dir, $filename) = $this->get_faildump_filename($event, 'png');
$context->saveScreenshot($filename, $dir);
}
}
/**
* Take a dump of the page content when a step fails.
*
* @throws Exception
* @param AfterStepTested $event
* @param \Behat\Context\Context\Context $context
*/
protected function take_contentdump(AfterStepTested $event, $context) {
list ($dir, $filename) = $this->get_faildump_filename($event, 'html');
$fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
fwrite($fh, $context->getMink()->getSession()->getPage()->getContent());
fclose($fh);
}
/**
* Determine the full pathname to store a failure-related dump.
*
* This is used for content such as the DOM, and screenshots.
*
* @param AfterStepTested $event
* @param String $filetype The file suffix to use. Limited to 4 chars.
*/
protected function get_faildump_filename(AfterStepTested $event, $filetype) {
// Make a directory for the scenario.
$featurename = $event->getFeature()->getTitle();
$featurename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $featurename);
if ($this->getParameter('dir_permissions')) {
$dirpermissions = $this->getParameter('dir_permissions');
} else {
$dirpermissions = 0777;
}
$dir = $this->get_run_screenshot_dir();
// We want a i-am-the-scenario-title format.
$dir = $dir . DIRECTORY_SEPARATOR . self::$currentscenariocount . '-' . $featurename;
if (!is_dir($dir) && !mkdir($dir, $dirpermissions, true)) {
// We already checked that the directory is writable. This should not fail.
throw new FormatterException(sprintf(
'No directories can be created inside %s, check the directory permissions.', $dir
));
}
// The failed step text.
// We want a stepno-i-am-the-failed-step.$filetype format.
$filename = $event->getStep()->getText();
$filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
$filename = self::$currentscenariostepcount . '-' . $filename;
// File name limited to 255 characters. Leaving 4 chars for the file
// extension as we allow .png for images and .html for DOM contents.
$filename = substr($filename, 0, 250) . '.' . $filetype;
return [$dir, $filename];
}
}
@@ -0,0 +1,155 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Output\Formatter;
use Behat\Behat\EventDispatcher\Event\AfterFeatureTested;
use Behat\Behat\EventDispatcher\Event\AfterStepTested;
use Behat\Behat\EventDispatcher\Event\BeforeFeatureTested;
use Behat\Testwork\Output\Formatter;
use Behat\Testwork\Output\Printer\OutputPrinter;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Feature step counter for distributing features between parallel runs.
*
* Use it with --dry-run (and any other selectors combination) to
* get the results quickly.
*
* @package core
* @copyright 2016 onwards Rajesh Taneja
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleStepcountFormatter implements Formatter {
/** @var int Number of steps executed in feature file. */
private static $stepcount = 0;
/** @var OutputPrinter */
private $printer;
/** @var array */
private $parameters;
/** @var string */
private $name;
/** @var string */
private $description;
/**
* Initializes formatter.
*
* @param string $name
* @param string $description
* @param array $parameters
* @param OutputPrinter $printer
*/
public function __construct($name, $description, array $parameters, OutputPrinter $printer) {
$this->name = $name;
$this->description = $description;
$this->parameters = $parameters;
$this->printer = $printer;
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* @return array The event names to listen to
*/
public static function getSubscribedEvents() {
return [
'tester.feature_tested.before' => 'beforeFeature',
'tester.feature_tested.after' => 'afterFeature',
'tester.step_tested.after' => 'afterStep',
];
}
/**
* Returns formatter name.
*
* @return string
*/
public function getName() {
return $this->name;
}
/**
* Returns formatter description.
*
* @return string
*/
public function getDescription() {
return $this->description;
}
/**
* Returns formatter output printer.
*
* @return OutputPrinter
*/
public function getOutputPrinter() {
return $this->printer;
}
/**
* Sets formatter parameter.
*
* @param string $name
* @param mixed $value
*/
public function setParameter($name, $value) {
$this->parameters[$name] = $value;
}
/**
* Returns parameter name.
*
* @param string $name
* @return mixed
*/
public function getParameter($name) {
return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
}
/**
* Listens to "feature.before" event.
*
* @param BeforeFeatureTested $event
*/
public function beforeFeature(BeforeFeatureTested $event) {
self::$stepcount = 0;
}
/**
* Listens to "feature.after" event.
*
* @param AfterFeatureTested $event
*/
public function afterFeature(AfterFeatureTested $event) {
$this->printer->writeln($event->getFeature()->getFile() . '::' . self::$stepcount);
}
/**
* Listens to "step.after" event.
*
* @param AfterStepTested $event
*/
public function afterStep(AfterStepTested $event) {
self::$stepcount++;
}
}
@@ -0,0 +1,133 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\Output\Printer;
use Behat\Behat\Output\Node\Printer\SetupPrinter;
use Behat\Testwork\Call\CallResult;
use Behat\Testwork\Hook\Tester\Setup\HookedTeardown;
use Behat\Testwork\Output\Formatter;
use Behat\Testwork\Output\Printer\OutputPrinter;
use Behat\Testwork\Tester\Setup\Setup;
use Behat\Testwork\Tester\Setup\Teardown;
use Moodle\BehatExtension\Driver\WebDriver;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Prints hooks in a pretty fashion.
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class MoodleProgressPrinter implements SetupPrinter {
/**
* @var string Moodle directory root.
*/
private $moodledirroot;
/**
* @var bool true if output is displayed.
*/
private static $outputdisplayed;
/**
* Constructor.
*
* @param string $moodledirroot Moodle dir root.
*/
public function __construct($moodledirroot) {
$this->moodledirroot = $moodledirroot;
}
/**
* Prints setup state.
*
* @param Formatter $formatter
* @param Setup $setup
*/
public function printSetup(Formatter $formatter, Setup $setup) {
if (empty(self::$outputdisplayed)) {
$this->printMoodleInfo($formatter->getOutputPrinter());
self::$outputdisplayed = true;
}
}
/**
* Prints teardown state.
*
* @param Formatter $formatter
* @param Teardown $teardown
*/
public function printTeardown(Formatter $formatter, Teardown $teardown) {
if (!$teardown instanceof HookedTeardown) {
return;
}
foreach ($teardown->getHookCallResults() as $callresult) {
$this->printTeardownHookCallResult($formatter->getOutputPrinter(), $callresult);
}
}
/**
* We print the site info + driver used and OS.
*
* @param Printer $printer
* @return void
*/
public function printMoodleInfo($printer) {
require_once($this->moodledirroot . '/lib/behat/classes/util.php');
$browser = WebDriver::getBrowserName();
// Calling all directly from here as we avoid more behat framework extensions.
$runinfo = \behat_util::get_site_info();
$runinfo .= 'Server OS "' . PHP_OS . '"' . ', Browser: "' . $browser . '"' . PHP_EOL;
$runinfo .= 'Started at ' . date('d-m-Y, H:i', time());
$printer->writeln($runinfo);
}
/**
* Prints teardown hook call result.
*
* @param OutputPrinter $printer
* @param CallResult $callresult
*/
private function printTeardownHookCallResult(OutputPrinter $printer, CallResult $callresult) {
// Notify dev that chained step is being used.
if (\Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester::is_chained_step_used()) {
$printer->writeln();
$printer->write(
"{+failed}Chained steps are deprecated. " .
"See https://moodledev.io/general/development/tools/behat/" .
"Migrating_from_Behat_2.5_to_3.x_in_Moodle#Changes_required_in_context_file{-failed}"
);
}
if (!$callresult->hasStdOut() && !$callresult->hasException()) {
return;
}
$hook = $callresult->getCall()->getCallee();
$path = $hook->getPath();
$printer->writeln($hook);
$printer->writeln($path);
}
}
@@ -0,0 +1,289 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace Moodle\BehatExtension\ServiceContainer;
use Behat\Behat\Definition\ServiceContainer\DefinitionExtension;
use Behat\Behat\EventDispatcher\ServiceContainer\EventDispatcherExtension;
use Behat\Behat\Gherkin\ServiceContainer\GherkinExtension;
use Behat\Behat\Tester\ServiceContainer\TesterExtension;
use Behat\Testwork\Cli\ServiceContainer\CliExtension;
use Behat\Testwork\Output\ServiceContainer\OutputExtension;
use Behat\Testwork\ServiceContainer\Extension as ExtensionInterface;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Behat\Testwork\ServiceContainer\ServiceProcessor;
use Behat\Testwork\Suite\ServiceContainer\SuiteExtension;
use Moodle\BehatExtension\Driver\WebDriverFactory;
use Moodle\BehatExtension\Output\Formatter\MoodleProgressFormatterFactory;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Behat extension for moodle
*
* Provides multiple features directory loading (Gherkin\Loader\MoodleFeaturesSuiteLoader
*
* @package core
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class BehatExtension implements ExtensionInterface {
/** @var string Extension configuration ID */
const MOODLE_ID = 'moodle';
/** @var ServiceProcessor */
private $processor;
/**
* Initializes compiler pass.
*
* @param null|ServiceProcessor $processor
*/
public function __construct(ServiceProcessor $processor = null) {
$this->processor = $processor ? : new ServiceProcessor();
}
/**
* Loads moodle specific configuration.
*
* @param ContainerBuilder $container ContainerBuilder instance
* @param array $config Extension configuration hash (from behat.yml)
*/
public function load(ContainerBuilder $container, array $config) {
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/services'));
$loader->load('core.xml');
// Getting the extension parameters.
$container->setParameter('behat.moodle.parameters', $config);
// Load moodle progress formatter.
$moodleprogressformatter = new MoodleProgressFormatterFactory();
$moodleprogressformatter->buildFormatter($container);
// Load custom step tester event dispatcher.
$this->loadEventDispatchingStepTester($container);
// Load chained step tester.
$this->loadChainedStepTester($container);
// Load step count formatter.
$this->loadMoodleListFormatter($container);
// Load step count formatter.
$this->loadMoodleStepcountFormatter($container);
// Load screenshot formatter.
$this->loadMoodleScreenshotFormatter($container);
// Load namespace alias.
$this->alias_old_namespaces();
}
/**
* Loads moodle List formatter.
*
* @param ContainerBuilder $container
*/
protected function loadMoodleListFormatter(ContainerBuilder $container) {
$definition = new Definition('Moodle\BehatExtension\Output\Formatter\MoodleListFormatter', [
'moodle_list',
'List all scenarios. Use with --dry-run',
['stepcount' => false],
$this->createOutputPrinterDefinition()
]);
$definition->addTag(OutputExtension::FORMATTER_TAG, ['priority' => 101]);
$container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodle_list', $definition);
}
/**
* Loads moodle Step count formatter.
*
* @param ContainerBuilder $container
*/
protected function loadMoodleStepcountFormatter(ContainerBuilder $container) {
$definition = new Definition('Moodle\BehatExtension\Output\Formatter\MoodleStepcountFormatter', [
'moodle_stepcount',
'Count steps in feature files. Use with --dry-run',
['stepcount' => false],
$this->createOutputPrinterDefinition()
]);
$definition->addTag(OutputExtension::FORMATTER_TAG, ['priority' => 101]);
$container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodle_stepcount', $definition);
}
/**
* Loads moodle screenshot formatter.
*
* @param ContainerBuilder $container
*/
protected function loadMoodleScreenshotFormatter(ContainerBuilder $container) {
$definition = new Definition('Moodle\BehatExtension\Output\Formatter\MoodleScreenshotFormatter', [
'moodle_screenshot',
// phpcs:ignore Generic.Files.LineLength.TooLong
'Take screenshot of all steps. Use --format-settings \'{"formats": "html,image"}\' to get specific o/p type',
['formats' => 'html,image'],
$this->createOutputPrinterDefinition()
]);
$definition->addTag(OutputExtension::FORMATTER_TAG, ['priority' => 102]);
$container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodle_screenshot', $definition);
}
/**
* Creates output printer definition.
*
* @return Definition
*/
protected function createOutputPrinterDefinition() {
return new Definition('Behat\Testwork\Output\Printer\StreamOutputPrinter', [
new Definition('Behat\Behat\Output\Printer\ConsoleOutputFactory'),
]);
}
/**
* Loads definition printers.
*
* @param ContainerBuilder $container
*/
private function loadDefinitionPrinters(ContainerBuilder $container) {
$definition = new Definition('Moodle\BehatExtension\Definition\Printer\ConsoleDefinitionInformationPrinter', [
new Reference(CliExtension::OUTPUT_ID),
new Reference(DefinitionExtension::PATTERN_TRANSFORMER_ID),
new Reference(DefinitionExtension::DEFINITION_TRANSLATOR_ID),
new Reference(GherkinExtension::KEYWORDS_ID)
]);
$container->removeDefinition('definition.information_printer');
$container->setDefinition('definition.information_printer', $definition);
}
/**
* Loads definition controller.
*
* @param ContainerBuilder $container
*/
private function loadController(ContainerBuilder $container) {
$definition = new Definition('Moodle\BehatExtension\Definition\Cli\AvailableDefinitionsController', [
new Reference(SuiteExtension::REGISTRY_ID),
new Reference(DefinitionExtension::WRITER_ID),
new Reference('definition.list_printer'),
new Reference('definition.information_printer')
]);
$container->removeDefinition(CliExtension::CONTROLLER_TAG . '.available_definitions');
$container->setDefinition(CliExtension::CONTROLLER_TAG . '.available_definitions', $definition);
}
/**
* Loads chained step tester.
*
* @param ContainerBuilder $container
*/
protected function loadChainedStepTester(ContainerBuilder $container) {
// Chained steps.
$definition = new Definition('Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester', [
new Reference(TesterExtension::STEP_TESTER_ID),
]);
$definition->addTag(TesterExtension::STEP_TESTER_WRAPPER_TAG, ['priority' => 100]);
$container->setDefinition(TesterExtension::STEP_TESTER_WRAPPER_TAG . '.substep', $definition);
}
/**
* Loads event-dispatching step tester.
*
* @param ContainerBuilder $container
*/
protected function loadEventDispatchingStepTester(ContainerBuilder $container) {
$definition = new Definition('Moodle\BehatExtension\EventDispatcher\Tester\MoodleEventDispatchingStepTester', [
new Reference(TesterExtension::STEP_TESTER_ID),
new Reference(EventDispatcherExtension::DISPATCHER_ID)
]);
$definition->addTag(TesterExtension::STEP_TESTER_WRAPPER_TAG, ['priority' => -9999]);
$container->setDefinition(TesterExtension::STEP_TESTER_WRAPPER_TAG . '.event_dispatching', $definition);
}
/**
* Setups configuration for current extension.
*
* @param ArrayNodeDefinition $builder
*/
public function configure(ArrayNodeDefinition $builder) {
// phpcs:disable PEAR.WhiteSpace.ObjectOperatorIndent.Incorrect
$builder->children()
->arrayNode('capabilities')
->useAttributeAsKey('key')
->prototype('variable')->end()
->end()
->arrayNode('steps_definitions')
->useAttributeAsKey('key')
->prototype('variable')->end()
->end()
->scalarNode('moodledirroot')
->defaultNull()
->end()
->end()
->end();
// phpcs:enable PEAR.WhiteSpace.ObjectOperatorIndent.Incorrect
}
/**
* Returns the extension config key.
*
* @return string
*/
public function getConfigKey() {
return self::MOODLE_ID;
}
/**
* Initializes other extensions.
*
* This method is called immediately after all extensions are activated but
* before any extension `configure()` method is called. This allows extensions
* to hook into the configuration of other extensions providing such an
* extension point.
*
* @param ExtensionManager $extensionmanager
*/
public function initialize(ExtensionManager $extensionmanager) {
if (null !== $minkextension = $extensionmanager->getExtension('mink')) {
$minkextension->registerDriverFactory(new WebDriverFactory());
}
}
/**
* You can modify the container here before it is dumped to PHP code.
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container) {
// Load controller for definition printing.
$this->loadDefinitionPrinters($container);
$this->loadController($container);
}
/**
* Alias old namespace of given. when and then for BC.
*/
private function alias_old_namespaces() {
class_alias('Moodle\\BehatExtension\\Context\\Step\\Given', 'Behat\\Behat\\Context\\Step\\Given', true);
class_alias('Moodle\\BehatExtension\\Context\\Step\\When', 'Behat\\Behat\\Context\\Step\\When', true);
class_alias('Moodle\\BehatExtension\\Context\\Step\\Then', 'Behat\\Behat\\Context\\Step\\Then', true);
}
}
@@ -0,0 +1,26 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="behat.moodle.parameters"></parameter>
<parameter key="moodle.context.initializer.class">Moodle\BehatExtension\Context\Initializer\MoodleAwareInitializer</parameter>
<parameter key="moodle.context.contextclass.classresolver.class">Moodle\BehatExtension\Context\ContextClass\ClassResolver</parameter>
<parameter key="behat.mink.selector.handler.class">Behat\Mink\Selector\SelectorsHandler</parameter>
</parameters>
<services>
<!-- Moodle context initializer -->
<service id="moodle.context.initializer" class="%moodle.context.initializer.class%">
<argument>%behat.moodle.parameters%</argument>
<tag name="context.initializer" />
</service>
<!-- Moodle class resolver for behat context -->
<service id="moodle.context.contextclass.classresolver" class="%moodle.context.contextclass.classresolver.class%">
<argument>%behat.moodle.parameters%</argument>
<tag name="context.class_resolver" />
</service>
</services>
</container>
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contexts initializer class
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use Behat\Behat\Context\BehatContext,
Behat\MinkExtension\Context\MinkContext,
Moodle\BehatExtension\Context\MoodleContext;
/**
* Loads main subcontexts
*
* Loading of moodle subcontexts is done by the Moodle extension
*
* Renamed from behat FeatureContext class according
* to Moodle coding styles conventions
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_init_context extends BehatContext {
/**
* Initializes subcontexts
*
* @param array $parameters context parameters (set them up through behat.yml)
* @return void
*/
public function __construct(array $parameters) {
$this->useContext('moodle', new MoodleContext($parameters));
}
}
@@ -0,0 +1,122 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Auto complete form field class.
*
* @package core_form
* @category test
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_text.php');
/**
* Auto complete form field.
*
* @package core_form
* @category test
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_autocomplete extends behat_form_text {
/**
* Sets the value to a field.
*
* @param string $value
* @return void
*/
public function set_value($value) {
if (!$this->running_javascript()) {
throw new coding_exception('Setting the value of an autocomplete field requires javascript.');
}
// Clear all current selections.
$rootnode = $this->field->getParent()->getParent();
$selections = $rootnode->findAll('css', '.form-autocomplete-selection [role=option]');
foreach (array_reverse($selections) as $selection) {
$selection->click();
$this->wait_for_pending_js();
}
$allowscreation = $this->field->hasAttribute('data-tags') && !empty($this->field->getAttribute('data-tags'));
$hasmultiple = $this->field->hasAttribute('data-multiple') && !empty($this->field->getAttribute('data-multiple'));
if ($hasmultiple && false !== strpos($value, ',')) {
// Commas have a special meaning as a value separator in 'multiple' autocomplete elements.
// To handle this we break the value up by comma, and enter it in chunks.
$values = explode(',', $value);
while ($value = array_shift($values)) {
$this->add_value(trim($value), $allowscreation);
}
} else {
$this->add_value(trim($value), $allowscreation);
}
}
/**
* Add a value to the autocomplete.
*
* @param string $value
* @param bool $allowscreation
*/
protected function add_value(string $value, bool $allowscreation): void {
$value = trim($value);
// Click into the field.
$this->field->click();
// Remove any existing text.
do {
behat_base::type_keys($this->session, [behat_keys::BACKSPACE, behat_keys::DELETE]);
} while (strlen($this->field->getValue()) > 0);
$this->wait_for_pending_js();
// Type in the new value.
behat_base::type_keys($this->session, str_split($value));
$this->wait_for_pending_js();
// If the autocomplete found suggestions, then it will have:
// 1) marked itself as expanded; and
// 2) have an aria-selected suggestion in the list.
$expanded = $this->field->getAttribute('aria-expanded');
$suggestion = $this->field->getParent()->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
if ($expanded && null !== $suggestion) {
// A suggestion was found.
// Click on the first item in the list.
$suggestion->click();
} else if ($allowscreation) {
// Press the return key to create a new entry.
behat_base::type_keys($this->session, [behat_keys::ENTER]);
} else {
throw new \InvalidArgumentException(
"Unable to find '{$value}' in the list of options, and unable to create a new option"
);
}
$this->wait_for_pending_js();
// Press the escape to close the autocomplete suggestions list.
behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
$this->wait_for_pending_js();
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Availability form field class.
*
* @package core_form
* @category test
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_textarea.php');
/**
* Availability form field class.
*
* @package core_form
* @category test
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_availability extends behat_form_textarea {
/**
* Sets the value(s) of an availability element.
*
* At present this only supports the following value 'Grouping: xxx' where
* xxx is the name of a grouping. Additional value types can be added as
* necessary.
*
* @param string $value Value code
* @return void
*/
public function set_value($value) {
global $DB;
$driver = $this->session->getDriver();
// Check the availability condition is currently unset - we don't yet
// support changing an existing one.
$existing = $this->get_value();
if ($existing && $existing !== '{"op":"&","c":[],"showc":[]}') {
throw new Exception('Cannot automatically set availability when ' .
'there is existing setting - must clear manually');
}
// Check the value matches a supported format.
$matches = array();
if (!preg_match('~^\s*([^:]*):\s*(.*?)\s*$~', $value, $matches)) {
throw new Exception('Value for availability field does not match correct ' .
'format. Example: "Grouping: G1"');
}
$type = $matches[1];
$param = $matches[2];
if ($this->running_javascript()) {
switch (strtolower($type)) {
case 'grouping' :
// Set a grouping condition.
$driver->click('//div[@class="availability-button"]/button');
$driver->click('//button[@id="availability_addrestriction_grouping"]');
$escparam = behat_context_helper::escape($param);
$nodes = $driver->find(
'//span[contains(concat(" " , @class, " "), " availability_grouping ")]//' .
'option[normalize-space(.) = ' . $escparam . ']');
if (count($nodes) != 1) {
throw new Exception('Cannot find grouping in dropdown' . count($nodes));
}
$node = reset($nodes);
$value = $node->getValue();
$driver->selectOption(
'//span[contains(concat(" " , @class, " "), " availability_grouping ")]//' .
'select', $value);
break;
default:
// We don't support other types yet. The test author must write
// manual 'click on that button, etc' commands.
throw new Exception('The availability type "' . $type .
'" is currently not supported - must set manually');
}
} else {
$courseid = $driver->getValue('//input[@name="course"]');
switch (strtolower($type)) {
case 'grouping' :
// Define result with one grouping condition.
$groupingid = $DB->get_field('groupings', 'id',
array('courseid' => $courseid, 'name' => $param));
$json = \core_availability\tree::get_root_json(array(
\availability_grouping\condition::get_json($groupingid)));
break;
default:
// We don't support other types yet.
throw new Exception('The availability type "' . $type .
'" is currently not supported - must set with JavaScript');
}
$driver->setValue('//textarea[@name="availabilityconditionsjson"]',
json_encode($json));
}
}
}
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Single checkbox form element.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_field.php');
/**
* Checkbox form field.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_checkbox extends behat_form_field {
/**
* Sets the value of a checkbox.
*
* Anything !empty() is considered checked.
*
* @param string $value
* @return void
*/
public function set_value($value) {
if (!empty($value) && !$this->field->isChecked()) {
if (!$this->running_javascript()) {
$this->field->check();
return;
}
// Check it if it should be checked and it is not.
$this->field->click();
} else if (empty($value) && $this->field->isChecked()) {
if (!$this->running_javascript()) {
$this->field->uncheck();
return;
}
// Uncheck if it is checked and shouldn't.
$this->field->click();
}
}
/**
* Returns whether the field is checked or not.
*
* @return bool True if it is checked and false if it's not.
*/
public function get_value() {
return $this->field->isChecked();
}
/**
* Is it enabled?
*
* @param string $expectedvalue Anything !empty() is considered checked.
* @return bool
*/
public function matches($expectedvalue = false) {
$ischecked = $this->field->isChecked();
// Any non-empty value provided means that it should be checked.
if (!empty($expectedvalue) && $ischecked) {
return true;
} else if (empty($expectedvalue) && !$ischecked) {
return true;
}
return false;
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Date form field class.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_group.php');
use Behat\Mink\Exception\ExpectationException;
/**
* Date form field.
*
* This class will be refactored in case we are interested in
* creating more complex formats to fill date and date-time fields.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_date extends behat_form_group {
/**
* Sets the value to a date field.
*
* @param string $value The value to be assigned to the date selector field. The string value must be either
* parsable into a UNIX timestamp or equal to 'disabled' (if disabling the date selector).
* @return void
* @throws ExpectationException If the value is invalid.
*/
public function set_value($value) {
if ($value === 'disabled') {
// Disable the given date selector field.
$this->set_child_field_value('enabled', false);
} else if (is_numeric($value)) { // The value is numeric (unix timestamp).
// Assign the mapped values to each form element in the date selector field.
foreach ($this->get_mapped_fields($value) as $childname => $childvalue) {
$this->set_child_field_value($childname, $childvalue);
if ($childname === 'enabled') {
// As soon as the form is enabled, reset the day to an existing one (1st). Without that
// undesired modifications (JS) happens when changing of month and day if
// the interim combination doesn't exists (for example, 31 March => 01 April).
// Note that instead of always setting the day to 1, this could be a little more
// clever, for example only changing when the day > 28, or only when the
// months (current or changed) have less days that the other. But that would
// require more complex calculations than the simpler line below.
$this->set_child_field_value('day', 1);
}
}
} else { // Invalid value.
// Get the name of the field.
$fieldname = $this->field->find('css', 'legend')->getHtml();
throw new ExpectationException("Invalid value for '{$fieldname}'", $this->session);
}
}
/**
* Returns the date field identifiers and the values that should be assigned to them.
*
* @param int $timestamp The UNIX timestamp
* @return array
*/
protected function get_mapped_fields(int $timestamp): array {
// Order is important, first enable, and then year -> month -> day
// (other order can lead to some transitions not working as expected,
// for example, changing from 15 June to 31 August, Behat ends with
// date being 1 August if the modification order is day, then month).
// Note that the behaviour described above is 100% reproducible
// manually, with the form (JS) auto-fixing things in the middle and
// leading to undesired final dates.
return [
'enabled' => true,
'year' => date('Y', $timestamp),
'month' => date('n', $timestamp),
'day' => date('j', $timestamp),
];
}
/**
* Sets a value to a child element in the date form field.
*
* @param string $childname The name of the child field
* @param string|bool $childvalue The value
*/
private function set_child_field_value(string $childname, $childvalue) {
// Find the given child form element in the date selector field.
$childelement = $this->field->find('css', "*[name$='[{$childname}]']");
if ($childelement) {
// Get the field instance for the given child form element.
$childinstance = $this->get_field_instance_for_element($childelement);
// Set the value to the child form element.
$childinstance->set_value($childvalue);
}
}
}
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Date time form field class.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_date.php');
/**
* Date time form field.
*
* This class will be refactored in case we are interested in
* creating more complex formats to fill date-time fields.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_date_time extends behat_form_date {
/**
* Returns the date field identifiers and the values that should be assigned to them.
*
* @param int $timestamp The UNIX timestamp
* @return array
*/
protected function get_mapped_fields(int $timestamp): array {
return array_merge(parent::get_mapped_fields($timestamp), [
'hour' => date('G', $timestamp),
'minute' => (int) date('i', $timestamp)
]);
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle editor field.
*
* @package core_form
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Behat\Mink\Element\NodeElement as NodeElement;
require_once(__DIR__ . '/behat_form_textarea.php');
/**
* Moodle editor field.
*
* @package core_form
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_editor extends behat_form_textarea {
/**
* Sets the value to a field.
*
* @param string $value
*/
public function set_value($value): void {
$editorid = $this->field->getAttribute('id');
if ($this->running_javascript()) {
$value = addslashes($value);
// This will be transported in JSON, which doesn't allow newlines in strings, so we must escape them.
$value = str_replace("\n", "\\n", $value);
behat_base::execute_in_matching_contexts('editor', 'set_editor_value', [
$editorid,
$value,
]);
} else {
parent::set_value($value);
}
}
/**
* Returns the current value of the select element.
*
* @return string
*/
public function get_value(): string {
if ($this->running_javascript()) {
// Give any listening editors a chance to persist the value to the textarea.
// Some editors only do this on form submission or similar events.
behat_base::execute_in_matching_contexts('editor', 'store_current_value', [
$this->field->getAttribute('id'),
]);
}
return parent::get_value();
}
/**
* Select all the text in the form field.
*
*/
public function select_text() {
// NodeElement.keyPress simply doesn't work.
if (!$this->running_javascript()) {
throw new coding_exception('Selecting text requires javascript.');
}
$editorid = $this->field->getAttribute('id');
$js = ' (function() {
var e = document.getElementById("'.$editorid.'editable"),
r = rangy.createRange(),
s = rangy.getSelection();
while ((e.firstChild !== null) && (e.firstChild.nodeType != document.TEXT_NODE)) {
e = e.firstChild;
}
e.focus();
r.selectNodeContents(e);
s.setSingleRange(r);
}()); ';
behat_base::execute_script_in_session($this->session, $js);
}
/**
* Matches the provided value against the current field value.
*
* @param string $expectedvalue
* @return bool The provided value matches the field value?
*/
public function matches($expectedvalue) {
// Fetch the actual value to save fetching it multiple times.
$actualvalue = $this->get_value();
if ($this->text_matches($expectedvalue, $actualvalue)) {
// The text is an exact match already.
return true;
}
if ($this->text_matches("<p>{$expectedvalue}</p>", $actualvalue)) {
// A text editor may silently wrap the content in p tags.
return true;
}
// Standardise both the expected value and the actual field value.
// We are likely dealing with HTML content, given this is an editor.
$expectedvalue = $this->standardise_html($expectedvalue);
$actualvalue = $this->standardise_html($actualvalue);
// Note: We don't need to worry about the floats here that we care about in text_matches.
// That condition isn't relevant to the content of an editor.
if ($expectedvalue === $actualvalue) {
return true;
}
return false;
}
/**
* Standardises the HTML content for comparison.
*
* @param string $html The HTML content to standardise
* @return string The standardised HTML content
*/
protected function standardise_html(string $html): string {
$document = new DOMDocument();
$errorstate = libxml_use_internal_errors(true);
// Format the whitespace nicely.
$document->preserveWhiteSpace = false;
$document->formatOutput = true;
// Wrap the content in a DIV element so that it is not parsed weirdly.
// Note: We must remove newlines too because DOMDocument does not do so, despite preserveWhiteSpace being false.
// Unfortunately this is slightly limited in that it will also remove newlines from <pre> content and similar.
$document->loadHTML(str_replace("\n", "", "<div>{$html}</div>"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$document->normalizeDocument();
libxml_clear_errors();
libxml_use_internal_errors($errorstate);
// Save the content of the 'div' element, removing the <div> and </div> tags at the start and end.
return trim(substr(
$document->saveHTML($document->getElementsByTagName('div')->item(0)),
5,
-6
));
}
}
+329
View File
@@ -0,0 +1,329 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generic moodleforms field.
*
* @package core_form
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Session;
/**
* Representation of a form field.
*
* Basically an interface with Mink session.
*
* @package core_form
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_field implements behat_session_interface {
// All of the functionality of behat_base is shared with form fields via the behat_session_trait trait.
use behat_session_trait;
/**
* @var Session Behat session.
*/
protected $session;
/**
* @var NodeElement The field DOM node to interact with.
*/
protected $field;
/**
* @var string The field's locator.
*/
protected $fieldlocator = false;
/**
* Returns the Mink session.
*
* @param string|null $name name of the session OR active session will be used
* @return \Behat\Mink\Session
*/
public function getSession($name = null) {
return $this->session;
}
/**
* General constructor with the node and the session to interact with.
*
* @param Session $session Reference to Mink session to traverse/modify the page DOM.
* @param NodeElement $fieldnode The field DOM node
* @return void
*/
public function __construct(Session $session, NodeElement $fieldnode) {
$this->session = $session;
$this->field = $fieldnode;
}
/**
* Sets the value to a field.
*
* @param string $value
* @return void
*/
public function set_value($value) {
// We delegate to the best guess, if we arrived here
// using the generic behat_form_field is because we are
// dealing with a fgroup element.
$instance = $this->guess_type();
return $instance->set_value($value);
}
/**
* Returns the current value of the select element.
*
* @return string
*/
public function get_value() {
// We delegate to the best guess, if we arrived here
// using the generic behat_form_field is because we are
// dealing with a fgroup element.
$instance = $this->guess_type();
return $instance->get_value();
}
/**
* Presses specific keyboard key.
*
* @param mixed $char could be either char ('b') or char-code (98)
* @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta')
*/
public function key_press($char, $modifier = null) {
// We delegate to the best guess, if we arrived here
// using the generic behat_form_field is because we are
// dealing with a fgroup element.
$instance = $this->guess_type();
$instance->field->keyDown($char, $modifier);
try {
$instance->field->keyPress($char, $modifier);
$instance->field->keyUp($char, $modifier);
} catch (\Facebook\WebDriver\Exception\WebDriverException $e) {
// If the JS handler attached to keydown or keypress destroys the element
// the later events may trigger errors because form element no longer exist
// or is not visible. Ignore such exceptions here.
} catch (\Behat\Mink\Exception\ElementNotFoundException $e) {
// Other Mink drivers can throw this for the same reason as above.
}
}
/**
* Generic match implementation
*
* Will work well with text-based fields, extension required
* for most of the other cases.
*
* @param string $expectedvalue
* @return bool The provided value matches the field value?
*/
public function matches($expectedvalue) {
// We delegate to the best guess, if we arrived here
// using the generic behat_form_field is because we are
// dealing with a fgroup element.
$instance = $this->guess_type();
return $instance->matches($expectedvalue);
}
/**
* Get the value of an attribute set on this field.
*
* @param string $name The attribute name
* @return string The attribute value
*/
public function get_attribute($name) {
return $this->field->getAttribute($name);
}
/**
* Guesses the element type we are dealing with in case is not a text-based element.
*
* This class is the generic field type, behat_field_manager::get_form_field()
* should be able to find the appropiate class for the field type, but
* in cases like moodle form group elements we can not find the type of
* the field through the DOM so we also need to take care of the
* different field types from here. If we need to deal with more complex
* moodle form elements we will need to refactor this simple HTML elements
* guess method.
*
* @return behat_form_field
*/
private function guess_type() {
return $this->get_field_instance_for_element($this->field);
}
/**
* Returns the appropriate form field object for a given node element.
*
* @param NodeElement $element The node element
* @return behat_form_field
*/
protected function get_field_instance_for_element(NodeElement $element): behat_form_field {
global $CFG;
// We default to the text-based field if nothing was detected.
if (!$type = behat_field_manager::guess_field_type($element, $this->session)) {
$type = 'text';
}
$classname = 'behat_form_' . $type;
$classpath = $CFG->dirroot . '/lib/behat/form_field/' . $classname . '.php';
require_once($classpath);
return new $classname($this->session, $element);
}
/**
* Returns whether the scenario is running in a browser that can run Javascript or not.
*
* @return bool
*/
protected function running_javascript() {
return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\BrowserKitDriver';
}
/**
* Waits for all the JS activity to be completed.
*
* @return bool Whether any JS is still pending completion.
*/
protected function wait_for_pending_js() {
if (!$this->running_javascript()) {
// JS is not available therefore there is nothing to wait for.
return false;
}
return behat_base::wait_for_pending_js_in_session($this->session);
}
/**
* Gets the field internal id used by selenium wire protocol.
*
* Only available when running_javascript().
*
* @throws coding_exception
* @return int
*/
protected function get_internal_field_id() {
if (!$this->running_javascript()) {
throw new coding_exception('You can only get an internal ID using the selenium driver.');
}
return $this->getSession()
->getDriver()
->getWebDriver()
->findElement(WebDriverBy::xpath($node->getXpath()))
->getID();
}
/**
* Checks if the provided text matches the field value.
*
* @param string $expectedvalue
* @param string|null $actualvalue The actual value. If not specified, this will be fetched from $this->get_value().
* @return bool
*/
protected function text_matches($expectedvalue, ?string $actualvalue = null): bool {
$actualvalue = $actualvalue ?? $this->get_value();
// Non strict string comparison.
if (trim($expectedvalue) == trim($actualvalue)) {
return true;
}
// Do one more matching attempt for floats that are valid with current decsep in use
// (let's continue non strict comparing them as strings, but once unformatted).
$expectedfloat = unformat_float(trim($expectedvalue), true);
$actualfloat = unformat_float(trim($actualvalue), true);
// If they aren't null or false, then we are good to be compared (basically is_numeric()).
$goodfloats = !is_null($expectedfloat) && ($expectedfloat !== false) &&
!is_null($actualfloat) && ($actualfloat !== false);
if ($goodfloats && ((string)$expectedfloat == (string)$actualfloat)) {
return true;
}
return false;
}
/**
* Gets the field locator.
*
* Defaults to the field label but you can
* specify other locators if you are interested.
*
* Public visibility as in most cases will be hard to
* use this method in a generic way, as fields can
* be selected using multiple ways (label, id, name...).
*
* @throws coding_exception
* @param string $locatortype
* @return string
*/
protected function get_field_locator($locatortype = false) {
if (!empty($this->fieldlocator)) {
return $this->fieldlocator;
}
$fieldid = $this->field->getAttribute('id');
// Defaults to label.
if ($locatortype == 'label' || $locatortype == false) {
$labelnode = $this->session->getPage()->find('xpath', "//label[@for='$fieldid']|//p[@id='{$fieldid}_label']");
// Exception only if $locatortype was specified.
if (!$labelnode && $locatortype == 'label') {
throw new coding_exception('Field with "' . $fieldid . '" id does not have a label.');
}
$this->fieldlocator = $labelnode->getText();
}
// Let's look for the name as a second option (more popular than
// id's when pointing to fields).
if (($locatortype == 'name' || $locatortype == false) &&
empty($this->fieldlocator)) {
$name = $this->field->getAttribute('name');
// Exception only if $locatortype was specified.
if (!$name && $locatortype == 'name') {
throw new coding_exception('Field with "' . $fieldid . '" id does not have a name attribute.');
}
$this->fieldlocator = $name;
}
// Otherwise returns the id if no specific locator type was provided.
if (empty($this->fieldlocator)) {
$this->fieldlocator = $fieldid;
}
return $this->fieldlocator;
}
}
@@ -0,0 +1,122 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* File manager form element.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_field.php');
/**
* File manager form field.
*
* Simple filemanager field manager to allow
* forms to be filled using TableNodes. It only
* adds files and checks the field contents in the
* root directory. If you want to run complex actions
* that involves subdirectories or other repositories
* than 'Upload a file' you should use steps related with
* behat_filepicker::i_add_file_from_repository_to_filemanager
* this is intended to be used with multi-field
*
* This field manager allows you to:
* - Get: A comma-separated list of the root directory
* file names, including folders.
* - Set: Add a file, in case you want to add more than
* one file you can always set two table rows using
* the same locator.
* - Match: A comma-separated list of file names.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_filemanager extends behat_form_field {
/**
* Gets the value.
*
* @return string A comma-separated list of the root directory file names.
*/
public function get_value() {
// Wait until DOM and JS is ready.
$this->session->wait(behat_base::get_timeout(), behat_base::PAGE_READY_JS);
// Get the label to restrict the files to this single form field.
$fieldlabel = $this->get_field_locator();
// Get the name of the current directory elements.
$xpath = "//p[normalize-space(.)='$fieldlabel']" .
"/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')]" .
"/descendant::div[@data-fieldtype = 'filemanager']" .
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename ')]";
// We don't need to wait here, also we don't have access to protected
// contexts find* methods.
$files = $this->session->getPage()->findAll('xpath', $xpath);
if (!$files) {
return '';
}
$filenames = array();
foreach ($files as $filenode) {
$filenames[] = $filenode->getText();
}
return implode(',', $filenames);
}
/**
* Sets the field value.
*
* @param string $value
* @return void
*/
public function set_value($value) {
// Getting the filemanager label from the DOM.
$fieldlabel = $this->get_field_locator();
// Getting the filepicker context and using the step definition
// to upload the requested file.
$uploadcontext = behat_context_helper::get('behat_repository_upload');
$uploadcontext->i_upload_file_to_filemanager($value, $fieldlabel);
}
/**
* Matches the provided filename/s against the current field value.
*
* If the filemanager contains more than one file the $expectedvalue
* value should include all the file names separating them by comma.
*
* @param string $expectedvalue
* @return bool The provided value matches the field value?
*/
public function matches($expectedvalue) {
return $this->text_matches($expectedvalue);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generic group field class.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_field.php');
/**
* Class to re-guess the field type as grouped fields can have different field types.
*
* When filling fields inside a fgroup field element we don't know what kind
* of field are we dealing with, so we should re-guess it as behat_form_field
* does.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_group extends behat_form_field {
}
@@ -0,0 +1,74 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Custom interaction with inplace editable elements.
*
* @package core_form
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_text.php');
/**
* Custom interaction with inplace editable elements.
*
* @package core_form
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_inplaceeditable extends behat_form_text {
/**
* Sets the value to a field.
*
* @param string $value
* @return void
*/
public function set_value($value) {
// Require JS to run this step.
self::require_javascript();
// Click to enable editing.
self::execute(
'behat_general::i_click_on_in_the',
[
'[data-inplaceeditablelink]',
'css_element',
$this->field,
'NodeElement',
]
);
// Note: It is not possible to use the NodeElement->keyDown() and related functions because
// this can trigger a focusOnElement call each time.
// Instead use the behat_base::type_keys() function.
// The inplace editable selects all existing content on focus.
// Clear the existing value.
self::type_keys($this->session, [behat_keys::BACKSPACE]);
// Type in the new value, followed by ENTER to save the value.
self::type_keys($this->session, array_merge(
str_split($value),
[behat_keys::ENTER]
));
}
}
@@ -0,0 +1,51 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_select.php');
/**
* Custom interaction with inplace editable elements of type select
*
* @package core_form
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_inplaceeditable_select extends behat_form_select {
/**
* Sets the value to a field
*
* @param string $value
*/
public function set_value($value): void {
// Require JS to run this step.
self::require_javascript();
// Enable editing.
self::execute('behat_general::i_click_on_in_the', [
'[data-inplaceeditablelink]',
'css_element',
$this->field,
'NodeElement',
]);
// After editing is enabled, set the select field value.
$select = $this->field->find('css', 'select');
$select->selectOption($value);
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Silly behat_form_select extension.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_select.php');
/**
* Allows interaction with fmodvisible form fields.
*
* Plain behat_form_select extension as is the same
* kind of field.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_modvisible extends behat_form_select {
}
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Silly behat_form_select extension.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_text.php');
/**
* Allows interaction with passwordunmask form fields.
*
* Plain behat_form_select extension as it is the same
* kind of field.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_passwordunmask extends behat_form_text {
/**
* Sets the value to a field.
*
* @param string $value
* @return void
*/
public function set_value($value) {
if (!$this->running_javascript()) {
$this->field->setValue($value);
return;
}
$id = $this->field->getAttribute('id');
$wrapper = $this->field->getParent()->getParent()->getParent()->find('css', '[data-passwordunmask="wrapper"]');
$wrapper->click();
$this->wait_for_pending_js();
behat_base::type_keys($this->session, str_split($value));
$this->wait_for_pending_js();
// Press enter key after setting password to save.
behat_base::type_keys($this->session, [behat_keys::ENTER]);
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Radio input form element.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_checkbox.php');
/**
* Radio input form field.
*
* Extends behat_form_checkbox as the set_value() behaviour
* is the same and it behaves closer to a checkbox than to
* a text field.
*
* This form field type can be added to forms as any other
* moodle form element, but it does not make sense without
* a group of radio inputs, so is hard to find it alone and
* detect it by behat_field_manager::get_form_field(), where is useful
* is when the default behat_form_field class is being used, it
* finds a input[type=radio] and it delegates set_value() and
* get_value() to behat_form_radio.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_radio extends behat_form_checkbox {
/**
* Returns the radio input value attribute.
*
* Here we can not extend behat_form_checkbox because
* isChecked() does internally a (bool)getValue() and
* it is not good for radio buttons.
*
* @return string The value attribute
*/
public function get_value() {
return $this->field->isSelected();
}
/**
* Sets the value of a radio
*
* Partially overwriting behat_form_checkbox
* implementation as when JS is disabled we
* can not check() and we should use setValue()
*
* @param string $value
* @return void
*/
public function set_value($value) {
if ($this->running_javascript()) {
// Check on radio button.
$this->field->click();
// Trigger the onchange event as triggered when 'selecting' the radio.
if (!empty($value) && !$this->field->isSelected()) {
$this->trigger_on_change();
}
} else {
// BrowserKit does not accept a check nor a click in an input[type=radio].
$this->field->setValue($this->field->getAttribute('value'));
}
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Single select form field class.
*
* @package core_form
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_field.php');
/**
* Single select form field.
*
* @package core_form
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_select extends behat_form_field {
/**
* Sets the value(s) of a select element.
*
* Seems an easy select, but there are lots of combinations
* of browsers and operative systems and each one manages the
* autosubmits and the multiple option selects in a different way.
*
* @param string $value plain value or comma separated values if multiple. Commas in values escaped with backslash.
* @return void
*/
public function set_value($value) {
// Is the select multiple?
$multiple = $this->field->hasAttribute('multiple');
// Here we select the option(s).
if ($multiple) {
// Split and decode values. Comma separated list of values allowed. With valuable commas escaped with backslash.
$options = preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', trim($value)));
// This is a multiple select, let's pass the multiple flag after first option.
$afterfirstoption = false;
foreach ($options as $option) {
$this->field->selectOption(trim($option), $afterfirstoption);
$afterfirstoption = true;
}
} else {
// By default, assume the passed value is a non-multiple option.
$this->field->selectOption(trim($value));
}
}
/**
* Returns the text of the currently selected options.
*
* @return string Comma separated if multiple options are selected. Commas in option texts escaped with backslash.
*/
public function get_value() {
return $this->get_selected_options();
}
/**
* Returns whether the provided argument matches the current value.
*
* @param mixed $expectedvalue
* @return bool
*/
public function matches($expectedvalue) {
$multiple = $this->field->hasAttribute('multiple');
// Same implementation as the parent if it is a single select.
if (!$multiple) {
$cleanexpectedvalue = trim($expectedvalue);
$selectedtext = trim($this->get_selected_options());
$selectedvalue = trim($this->get_selected_options(false));
if ($cleanexpectedvalue != $selectedvalue && $cleanexpectedvalue != $selectedtext) {
return false;
}
return true;
}
// We are dealing with a multi-select.
// Unescape + trim all options and flip it to have the expected values as keys.
$expectedoptions = $this->get_unescaped_options($expectedvalue);
// Get currently selected option's texts.
$texts = $this->get_selected_options(true);
$selectedoptiontexts = $this->get_unescaped_options($texts);
// Get currently selected option's values.
$values = $this->get_selected_options(false);
$selectedoptionvalues = $this->get_unescaped_options($values);
// We check against string-ordered lists of options.
if ($expectedoptions !== $selectedoptiontexts &&
$expectedoptions !== $selectedoptionvalues) {
return false;
}
return true;
}
/**
* Cleans the list of options and returns it as a string separating options with |||.
*
* @param string $value The string containing the escaped options.
* @return string The options
*/
protected function get_unescaped_options($value) {
// Can be multiple comma separated, with valuable commas escaped with backslash.
$optionsarray = array_map(
'trim',
preg_replace('/\\\,/', ',',
preg_split('/(?<!\\\),/', $value)
)
);
// Sort by value (keeping the keys is irrelevant).
core_collator::asort($optionsarray, SORT_STRING);
// Returning it as a string which is easier to match against other values.
return implode('|||', $optionsarray);
}
/**
* Returns the field selected values.
*
* Externalized from the common behat_form_field API method get_value() as
* matches() needs to check against both values and texts.
*
* @param bool $returntexts Returns the options texts or the options values.
* @return string
*/
protected function get_selected_options($returntexts = true) {
$method = 'getHtml';
if ($returntexts === false) {
$method = 'getValue';
}
// Is the select multiple?
$multiple = $this->field->hasAttribute('multiple');
$selectedoptions = array(); // To accumulate found selected options.
// Driver returns the values as an array or as a string depending
// on whether multiple options are selected or not.
$values = $this->field->getValue();
if (!is_array($values)) {
$values = array($values);
}
// Get all the options in the select and extract their value/text pairs.
$alloptions = $this->field->findAll('xpath', '//option');
foreach ($alloptions as $option) {
// Is it selected?
if (in_array($option->getValue(), $values)) {
if ($multiple) {
// If the select is multiple, text commas must be encoded.
$selectedoptions[] = trim(str_replace(',', '\,', $option->{$method}()));
} else {
$selectedoptions[] = trim($option->{$method}());
}
}
}
return implode(', ', $selectedoptions);
}
/**
* Returns the opton XPath based on it's select xpath.
*
* @param string $option
* @param string $selectxpath
* @return string xpath
*/
protected function get_option_xpath($option, $selectxpath) {
$valueliteral = behat_context_helper::escape(trim($option));
return $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]";
}
}
@@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
require_once(__DIR__ . '/behat_form_field.php');
use \Behat\Mink\Element\NodeElement;
/**
* Custom interaction with select_menu elements
*
* @package core_form
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_select_menu extends behat_form_field {
/**
* Sets the value of the select menu field.
*
* @param string $value The string that is used to identify an option within the select menu. If the string
* has two items separated by '>' (ex. "Group > Option"), the first item ("Group") will be
* used to identify a particular group within the select menu, while the second ("Option")
* will be used to identify an option within that group. Otherwise, a string with a single
* item (ex. "Option") will be used to identify an option within the select menu regardless
* of any existing groups.
*/
public function set_value($value) {
self::require_javascript();
$this->field->click();
$option = $this->find_option($value);
$option->click();
}
public function get_value() {
$rootnode = $this->field->getParent();
$input = $rootnode->find('css', 'input');
return $input->getValue();
}
/**
* Checks whether a given option exists in the select menu field.
*
* @param string $option The string that is used to identify an option within the select menu. If the string
* has two items separated by '>' (ex. "Group > Option"), the first item ("Group") will be
* used to identify a particular group within the select menu, while the second ("Option")
* will be used to identify an option within that group. Otherwise, a string with a single
* item (ex. "Option") will be used to identify an option within the select menu regardless
* of any existing groups.
* @return bool Whether the option exists in the select menu field or not.
*/
public function has_option(string $option): bool {
if ($this->find_option($option)) {
return true;
}
return false;
}
/**
* Finds and returns a given option from the select menu field.
*
* @param string $option The string that is used to identify an option within the select menu. If the string
* has two items separated by '>' (ex. "Group > Option"), the first item ("Group") will be
* used to identify a particular group within the select menu, while the second ("Option")
* will be used to identify an option within that group. Otherwise, a string with a single
* item (ex. "Option") will be used to identify an option within the select menu regardless
* of any existing groups.
* @return NodeElement|null The option element or null if it cannot be found.
*/
private function find_option(string $option): ?NodeElement {
// Split the value string by ">" to determine whether a group has been specified.
$path = preg_split('/\s*>\s*/', trim($option));
if (count($path) > 1) { // Group has been specified.
$optionxpath = '//li[contains(@role, "presentation") and normalize-space(text()) = "' .
$this->escape($path[0]) . '"]' .
'/following-sibling::li[contains(@role, "option") and normalize-space(text()) = "' .
$this->escape($path[1]) . '"]';
} else { // Group has not been specified.
$optionxpath = '//li[contains(@role, "option") and normalize-space(text()) = "' .
$this->escape($path[0]) . '"]';
}
return $this->field->getParent()->find('xpath', $optionxpath);
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Silly behat_form_select extension.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_select.php');
/**
* Allows interaction with selectyesno form fields.
*
* Plain behat_form_select extension as it is the same
* kind of field.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_selectyesno extends behat_form_select {
}
+69
View File
@@ -0,0 +1,69 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Text field class.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_field.php');
/**
* Class for test-based fields.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_text extends behat_form_field {
/**
* Sets the value to a field.
*
* @param string $value
* @return void
*/
public function set_value($value) {
$this->field->setValue($value);
}
/**
* Returns the current value of the element.
*
* @return string
*/
public function get_value() {
return $this->field->getValue();
}
/**
* Matches the provided value against the current field value.
*
* @param string $expectedvalue
* @return bool The provided value matches the field value?
*/
public function matches($expectedvalue) {
return $this->text_matches($expectedvalue);
}
}
@@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Textarea field class.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_text.php');
/**
* Textarea field class.
*
* @package core_form
* @category test
* @copyright 2014 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_textarea extends behat_form_text {
}
+586
View File
@@ -0,0 +1,586 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat basic functions
*
* It does not include MOODLE_INTERNAL because is part of the bootstrap.
*
* This script should not be usually included, neither any of its functions
* used, within mooodle code at all. It's for exclusive use of behat and
* moodle setup.php. For places requiring a different/special behavior
* needing to check if are being run as part of behat tests, use:
* if (defined('BEHAT_SITE_RUNNING')) { ...
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../testing/lib.php');
define('BEHAT_EXITCODE_CONFIG', 250);
define('BEHAT_EXITCODE_REQUIREMENT', 251);
define('BEHAT_EXITCODE_PERMISSIONS', 252);
define('BEHAT_EXITCODE_REINSTALL', 253);
define('BEHAT_EXITCODE_INSTALL', 254);
define('BEHAT_EXITCODE_INSTALLED', 256);
/**
* The behat test site fullname and shortname.
*/
define('BEHAT_PARALLEL_SITE_NAME', "behatrun");
/**
* Exits with an error code
*
* @param mixed $errorcode
* @param string $text
* @return void Stops execution with error code
*/
function behat_error($errorcode, $text = '') {
// Adding error prefixes.
switch ($errorcode) {
case BEHAT_EXITCODE_CONFIG:
$text = 'Behat config error: ' . $text;
break;
case BEHAT_EXITCODE_REQUIREMENT:
$text = 'Behat requirement not satisfied: ' . $text;
break;
case BEHAT_EXITCODE_PERMISSIONS:
$text = 'Behat permissions problem: ' . $text . ', check the permissions';
break;
case BEHAT_EXITCODE_REINSTALL:
$path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
$text = "Reinstall Behat: ".$text.", use:\n php ".$path;
break;
case BEHAT_EXITCODE_INSTALL:
$path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
$text = "Install Behat before enabling it, use:\n php ".$path;
break;
case BEHAT_EXITCODE_INSTALLED:
$text = "The Behat site is already installed";
break;
default:
$text = 'Unknown error ' . $errorcode . ' ' . $text;
break;
}
testing_error($errorcode, $text);
}
/**
* Return logical error string.
*
* @param int $errtype php error type.
* @return string string which will be returned.
*/
function behat_get_error_string($errtype) {
switch ($errtype) {
case E_USER_ERROR:
$errnostr = 'Fatal error';
break;
case E_WARNING:
case E_USER_WARNING:
$errnostr = 'Warning';
break;
case E_NOTICE:
case E_USER_NOTICE:
case E_STRICT:
$errnostr = 'Notice';
break;
case E_RECOVERABLE_ERROR:
$errnostr = 'Catchable';
break;
default:
$errnostr = 'Unknown error type';
}
return $errnostr;
}
/**
* PHP errors handler to use when running behat tests.
*
* Adds specific CSS classes to identify
* the messages.
*
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
* @return bool
*/
function behat_error_handler($errno, $errstr, $errfile, $errline) {
// If is preceded by an @ we don't show it.
if (!error_reporting()) {
return true;
}
// This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is
// set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current
// error_reporting() value does not include one of those levels is because it has been forced through
// the moodle code (see fix_utf8() for example) in that cases we respect the forced error level value.
$respect = array(E_NOTICE, E_USER_NOTICE, E_STRICT, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED);
foreach ($respect as $respectable) {
// If the current value does not include this kind of errors and the reported error is
// at that level don't print anything.
if ($errno == $respectable && !(error_reporting() & $respectable)) {
return true;
}
}
// Using the default one in case there is a fatal catchable error.
default_error_handler($errno, $errstr, $errfile, $errline);
$errnostr = behat_get_error_string($errno);
// If ajax script then throw exception, so the calling api catch it and show it on web page.
if (defined('AJAX_SCRIPT')) {
throw new Exception("$errnostr: $errstr in $errfile on line $errline");
} else {
// Wrapping the output.
echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL;
echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL;
echo '</div>';
}
// Also use the internal error handler so we keep the usual behaviour.
return false;
}
/**
* Before shutdown save last error entries, so we can fail the test.
*/
function behat_shutdown_function() {
// If any error found, then save it.
if ($error = error_get_last()) {
// Ignore E_WARNING, as they might come via ( @ )suppression and might lead to false failure.
if (isset($error['type']) && !($error['type'] & E_WARNING)) {
$errors = behat_get_shutdown_process_errors();
$errors[] = $error;
$errorstosave = json_encode($errors);
set_config('process_errors', $errorstosave, 'tool_behat');
}
}
}
/**
* Return php errors save which were save during shutdown.
*
* @return array
*/
function behat_get_shutdown_process_errors() {
global $DB;
// Don't use get_config, as it use cache and return invalid value, between selenium and cli process.
$phperrors = $DB->get_field('config_plugins', 'value', array('name' => 'process_errors', 'plugin' => 'tool_behat'));
if (!empty($phperrors)) {
return json_decode($phperrors, true);
} else {
return array();
}
}
/**
* Restrict the config.php settings allowed.
*
* When running the behat features the config.php
* settings should not affect the results.
*
* @return void
*/
function behat_clean_init_config() {
global $CFG;
$allowed = array_flip(array(
'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
'proxybypass', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class', 'pathtopython'
));
// Add extra allowed settings.
if (!empty($CFG->behat_extraallowedsettings)) {
$allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
}
// Also allowing behat_ prefixed attributes.
foreach ($CFG as $key => $value) {
if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
unset($CFG->{$key});
}
}
}
/**
* Checks that the behat config vars are properly set.
*
* @return void Stops execution with error code if something goes wrong.
*/
function behat_check_config_vars() {
global $CFG;
$moodleprefix = empty($CFG->prefix) ? '' : $CFG->prefix;
$behatprefix = empty($CFG->behat_prefix) ? '' : $CFG->behat_prefix;
$phpunitprefix = empty($CFG->phpunit_prefix) ? '' : $CFG->phpunit_prefix;
$behatdbname = empty($CFG->behat_dbname) ? $CFG->dbname : $CFG->behat_dbname;
$phpunitdbname = empty($CFG->phpunit_dbname) ? $CFG->dbname : $CFG->phpunit_dbname;
$behatdbhost = empty($CFG->behat_dbhost) ? $CFG->dbhost : $CFG->behat_dbhost;
$phpunitdbhost = empty($CFG->phpunit_dbhost) ? $CFG->dbhost : $CFG->phpunit_dbhost;
// Verify prefix value.
if (empty($CFG->behat_prefix)) {
behat_error(BEHAT_EXITCODE_CONFIG,
'Define $CFG->behat_prefix in config.php');
}
if ($behatprefix == $moodleprefix && $behatdbname == $CFG->dbname && $behatdbhost == $CFG->dbhost) {
behat_error(BEHAT_EXITCODE_CONFIG,
'$CFG->behat_prefix in config.php must be different from $CFG->prefix' .
' when $CFG->behat_dbname and $CFG->behat_host are not set or when $CFG->behat_dbname equals $CFG->dbname' .
' and $CFG->behat_dbhost equals $CFG->dbhost');
}
if ($phpunitprefix !== '' && $behatprefix == $phpunitprefix && $behatdbname == $phpunitdbname &&
$behatdbhost == $phpunitdbhost) {
behat_error(BEHAT_EXITCODE_CONFIG,
'$CFG->behat_prefix in config.php must be different from $CFG->phpunit_prefix' .
' when $CFG->behat_dbname equals $CFG->phpunit_dbname' .
' and $CFG->behat_dbhost equals $CFG->phpunit_dbhost');
}
// Verify behat wwwroot value.
if (empty($CFG->behat_wwwroot)) {
behat_error(BEHAT_EXITCODE_CONFIG,
'Define $CFG->behat_wwwroot in config.php');
}
if (!empty($CFG->wwwroot) and $CFG->behat_wwwroot == $CFG->wwwroot) {
behat_error(BEHAT_EXITCODE_CONFIG,
'$CFG->behat_wwwroot in config.php must be different from $CFG->wwwroot');
}
// Verify behat dataroot value.
if (empty($CFG->behat_dataroot)) {
behat_error(BEHAT_EXITCODE_CONFIG,
'Define $CFG->behat_dataroot in config.php');
}
clearstatcache();
if (!file_exists($CFG->behat_dataroot_parent)) {
$permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
umask(0);
if (!mkdir($CFG->behat_dataroot_parent, $permissions, true)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
}
}
$CFG->behat_dataroot_parent = realpath($CFG->behat_dataroot_parent);
if (empty($CFG->behat_dataroot_parent) or !is_dir($CFG->behat_dataroot_parent) or !is_writable($CFG->behat_dataroot_parent)) {
behat_error(BEHAT_EXITCODE_CONFIG,
'$CFG->behat_dataroot in config.php must point to an existing writable directory');
}
if (!empty($CFG->dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->dataroot)) {
behat_error(BEHAT_EXITCODE_CONFIG,
'$CFG->behat_dataroot in config.php must be different from $CFG->dataroot');
}
if (!empty($CFG->phpunit_dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->phpunit_dataroot)) {
behat_error(BEHAT_EXITCODE_CONFIG,
'$CFG->behat_dataroot in config.php must be different from $CFG->phpunit_dataroot');
}
// This request is coming from admin/tool/behat/cli/util.php which will call util_single.php. So just return from
// here as we don't need to create a dataroot for single run.
if (defined('BEHAT_PARALLEL_UTIL') && BEHAT_PARALLEL_UTIL && empty($CFG->behatrunprocess)) {
return;
}
if (!file_exists($CFG->behat_dataroot)) {
$permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
umask(0);
if (!mkdir($CFG->behat_dataroot, $permissions, true)) {
behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
}
}
$CFG->behat_dataroot = realpath($CFG->behat_dataroot);
}
/**
* Should we switch to the test site data?
* @return bool
*/
function behat_is_test_site() {
global $CFG;
if (defined('BEHAT_UTIL')) {
// This is the admin tool that installs/drops the test site install.
return true;
}
if (defined('BEHAT_TEST')) {
// This is the main vendor/bin/behat script.
return true;
}
if (empty($CFG->behat_wwwroot)) {
return false;
}
if (isset($_SERVER['REMOTE_ADDR']) and behat_is_requested_url($CFG->behat_wwwroot)) {
// Something is accessing the web server like a real browser.
return true;
}
return false;
}
/**
* Fix variables for parallel behat testing.
* - behat_wwwroot = behat_wwwroot{behatrunprocess}
* - behat_dataroot = behat_dataroot{behatrunprocess}
* - behat_prefix = behat_prefix.{behatrunprocess}_ (For oracle it will be firstletter of prefix and behatrunprocess)
**/
function behat_update_vars_for_process() {
global $CFG;
$allowedconfigoverride = array('dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'behat_prefix',
'behat_wwwroot', 'behat_dataroot');
$behatrunprocess = behat_get_run_process();
$CFG->behatrunprocess = $behatrunprocess;
// Data directory will be a directory under parent directory.
$CFG->behat_dataroot_parent = $CFG->behat_dataroot;
$CFG->behat_dataroot .= '/'. BEHAT_PARALLEL_SITE_NAME;
if ($behatrunprocess) {
if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_wwwroot'])) {
// Set www root for run process.
if (isset($CFG->behat_wwwroot) &&
!preg_match("#/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess . "\$#", $CFG->behat_wwwroot)) {
$CFG->behat_wwwroot .= "/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess;
}
}
if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_dataroot'])) {
// Set behat_dataroot.
if (!preg_match("#" . $behatrunprocess . "\$#", $CFG->behat_dataroot)) {
$CFG->behat_dataroot .= $behatrunprocess;
}
}
// Set behat_prefix for db, just suffix run process number, to avoid max length exceed.
// For oracle only 2 letter prefix is possible.
// NOTE: This will not work for parallel process > 9.
if ($CFG->dbtype === 'oci') {
$CFG->behat_prefix = substr($CFG->behat_prefix, 0, 1);
$CFG->behat_prefix .= "{$behatrunprocess}";
} else {
$CFG->behat_prefix .= "{$behatrunprocess}_";
}
if (!empty($CFG->behat_parallel_run[$behatrunprocess - 1])) {
// Override allowed config vars.
foreach ($allowedconfigoverride as $config) {
if (isset($CFG->behat_parallel_run[$behatrunprocess - 1][$config])) {
$CFG->$config = $CFG->behat_parallel_run[$behatrunprocess - 1][$config];
}
}
}
}
}
/**
* Checks if the URL requested by the user matches the provided argument
*
* @param string $url
* @return bool Returns true if it matches.
*/
function behat_is_requested_url($url) {
$parsedurl = parse_url($url . '/');
if (!isset($parsedurl['port'])) {
$parsedurl['port'] = ($parsedurl['scheme'] === 'https') ? 443 : 80;
}
$parsedurl['path'] = rtrim($parsedurl['path'], '/');
// Removing the port.
$pos = strpos($_SERVER['HTTP_HOST'], ':');
if ($pos !== false) {
$requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos);
} else {
$requestedhost = $_SERVER['HTTP_HOST'];
}
// The path should also match.
if (empty($parsedurl['path'])) {
$matchespath = true;
} else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) {
$matchespath = true;
}
// The host and the port should match
if ($parsedurl['host'] == $requestedhost && $parsedurl['port'] == $_SERVER['SERVER_PORT'] && !empty($matchespath)) {
return true;
}
return false;
}
/**
* Get behat run process from either $_SERVER or command config.
*
* @return bool|int false if single run, else run process number.
*/
function behat_get_run_process() {
global $argv, $CFG;
$behatrunprocess = false;
// Get behat run process, if set.
if (defined('BEHAT_CURRENT_RUN') && BEHAT_CURRENT_RUN) {
$behatrunprocess = BEHAT_CURRENT_RUN;
} else if (!empty($_SERVER['REMOTE_ADDR'])) {
// Try get it from config if present.
if (!empty($CFG->behat_parallel_run)) {
foreach ($CFG->behat_parallel_run as $run => $behatconfig) {
if (isset($behatconfig['behat_wwwroot']) && behat_is_requested_url($behatconfig['behat_wwwroot'])) {
$behatrunprocess = $run + 1; // We start process from 1.
break;
}
}
}
// Check if parallel site prefix is used.
if (empty($behatrunprocess) && preg_match('#/' . BEHAT_PARALLEL_SITE_NAME . '(.+?)/#', $_SERVER['REQUEST_URI'])) {
$dirrootrealpath = str_replace("\\", "/", realpath($CFG->dirroot));
$serverrealpath = str_replace("\\", "/", realpath($_SERVER['SCRIPT_FILENAME']));
$afterpath = str_replace($dirrootrealpath.'/', '', $serverrealpath);
if (!$behatrunprocess = preg_filter("#.*/" . BEHAT_PARALLEL_SITE_NAME . "(.+?)/$afterpath#", '$1',
$_SERVER['SCRIPT_FILENAME'])) {
throw new Exception("Unable to determine behat process [afterpath=" . $afterpath .
", scriptfilename=" . $_SERVER['SCRIPT_FILENAME'] . "]!");
}
}
} else if (defined('BEHAT_TEST') || defined('BEHAT_UTIL')) {
$behatconfig = '';
if ($match = preg_filter('#--run=(.+)#', '$1', $argv)) {
// Try to guess the run from the existence of the --run arg.
$behatrunprocess = reset($match);
} else {
// Try to guess the run from the existence of the --config arg. Note there are 2 alternatives below.
if ($k = array_search('--config', $argv)) {
// Alternative 1: --config /path/to/config.yml => (next arg, pick it).
$behatconfig = str_replace("\\", "/", $argv[$k + 1]);
} else if ($config = preg_filter('#^(?:--config[ =]*)(.+)$#', '$1', $argv)) {
// Alternative 2: --config=/path/to/config.yml => (same arg, just get the path part).
$behatconfig = str_replace("\\", "/", reset($config));
}
// Try get it from config if present.
if ($behatconfig) {
if (!empty($CFG->behat_parallel_run)) {
foreach ($CFG->behat_parallel_run as $run => $parallelconfig) {
if (!empty($parallelconfig['behat_dataroot']) &&
$parallelconfig['behat_dataroot'] . '/behat/behat.yml' == $behatconfig) {
$behatrunprocess = $run + 1; // We start process from 1.
break;
}
}
}
// Check if default behat dataroot increment was done.
if (empty($behatrunprocess)) {
$behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot . '/' . BEHAT_PARALLEL_SITE_NAME);
$behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1',
$behatconfig);
}
}
}
}
return $behatrunprocess;
}
/**
* Execute commands in parallel.
*
* @param array $cmds list of commands to be executed.
* @param string $cwd absolute path of working directory.
* @param int $delay time in seconds to add delay between each parallel process.
* @return array list of processes.
*/
function cli_execute_parallel($cmds, $cwd = null, $delay = 0) {
require_once(__DIR__ . "/../../vendor/autoload.php");
$processes = array();
// Create child process.
foreach ($cmds as $name => $cmd) {
if (method_exists('\\Symfony\\Component\\Process\\Process', 'fromShellCommandline')) {
// Process 4.2 and up.
$process = Symfony\Component\Process\Process::fromShellCommandline($cmd);
} else {
// Process 4.1 and older.
$process = new Symfony\Component\Process\Process(null);
$process->setCommandLine($cmd);
}
$process->setWorkingDirectory($cwd);
$process->setTimeout(null);
$processes[$name] = $process;
$processes[$name]->start();
// If error creating process then exit.
if ($processes[$name]->getStatus() !== 'started') {
echo "Error starting process: $name";
foreach ($processes[$name] as $process) {
if ($process) {
$process->signal(SIGKILL);
}
}
exit(1);
}
// Sleep for specified delay.
if ($delay) {
sleep($delay);
}
}
return $processes;
}
/**
* Get command flags for an option/value combination
*
* @param string $option
* @param string|bool|null $value
* @return string
*/
function behat_get_command_flags(string $option, $value): string {
$commandoptions = '';
if (is_bool($value)) {
if ($value) {
return " --{$option}";
} else {
return " --no-{$option}";
}
} else if ($value !== null) {
return " --$option=\"$value\"";
}
return '';
}