| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Behat hooks steps definitions.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * This methods are used by Behat CLI command.
 | 
        
           |  |  | 21 |  *
 | 
        
           |  |  | 22 |  * @package    core
 | 
        
           |  |  | 23 |  * @category   test
 | 
        
           |  |  | 24 |  * @copyright  2012 David Monllaó
 | 
        
           |  |  | 25 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 26 |  */
 | 
        
           |  |  | 27 |   | 
        
           |  |  | 28 | // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 | require_once(__DIR__ . '/../../behat/behat_base.php');
 | 
        
           |  |  | 31 |   | 
        
           |  |  | 32 | use Behat\Testwork\Hook\Scope\BeforeSuiteScope,
 | 
        
           |  |  | 33 |     Behat\Testwork\Hook\Scope\AfterSuiteScope,
 | 
        
           |  |  | 34 |     Behat\Behat\Hook\Scope\BeforeFeatureScope,
 | 
        
           |  |  | 35 |     Behat\Behat\Hook\Scope\AfterFeatureScope,
 | 
        
           |  |  | 36 |     Behat\Behat\Hook\Scope\BeforeScenarioScope,
 | 
        
           |  |  | 37 |     Behat\Behat\Hook\Scope\AfterScenarioScope,
 | 
        
           |  |  | 38 |     Behat\Behat\Hook\Scope\BeforeStepScope,
 | 
        
           |  |  | 39 |     Behat\Behat\Hook\Scope\AfterStepScope,
 | 
        
           |  |  | 40 |     Behat\Mink\Exception\ExpectationException,
 | 
        
           |  |  | 41 |     Behat\Mink\Exception\DriverException,
 | 
        
           |  |  | 42 |     Facebook\WebDriver\Exception\UnexpectedAlertOpenException,
 | 
        
           |  |  | 43 |     Facebook\WebDriver\Exception\WebDriverCurlException,
 | 
        
           |  |  | 44 |     Facebook\WebDriver\Exception\UnknownErrorException;
 | 
        
           |  |  | 45 |   | 
        
           |  |  | 46 | /**
 | 
        
           |  |  | 47 |  * Hooks to the behat process.
 | 
        
           |  |  | 48 |  *
 | 
        
           |  |  | 49 |  * Behat accepts hooks after and before each
 | 
        
           |  |  | 50 |  * suite, feature, scenario and step.
 | 
        
           |  |  | 51 |  *
 | 
        
           |  |  | 52 |  * They can not call other steps as part of their process
 | 
        
           |  |  | 53 |  * like regular steps definitions does.
 | 
        
           |  |  | 54 |  *
 | 
        
           |  |  | 55 |  * Throws generic Exception because they are captured by Behat.
 | 
        
           |  |  | 56 |  *
 | 
        
           |  |  | 57 |  * @package   core
 | 
        
           |  |  | 58 |  * @category  test
 | 
        
           |  |  | 59 |  * @copyright 2012 David Monllaó
 | 
        
           |  |  | 60 |  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 61 |  */
 | 
        
           |  |  | 62 | class behat_hooks extends behat_base {
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |     /**
 | 
        
           |  |  | 65 |      * @var For actions that should only run once.
 | 
        
           |  |  | 66 |      */
 | 
        
           |  |  | 67 |     protected static $initprocessesfinished = false;
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |     /** @var bool Whether the first javascript scenario has been seen yet */
 | 
        
           |  |  | 70 |     protected static $firstjavascriptscenarioseen = false;
 | 
        
           |  |  | 71 |   | 
        
           |  |  | 72 |     /**
 | 
        
           |  |  | 73 |      * @var bool Scenario running
 | 
        
           |  |  | 74 |      */
 | 
        
           |  |  | 75 |     protected $scenariorunning = false;
 | 
        
           |  |  | 76 |   | 
        
           |  |  | 77 |     /**
 | 
        
           |  |  | 78 |      * Some exceptions can only be caught in a before or after step hook,
 | 
        
           |  |  | 79 |      * they can not be thrown there as they will provoke a framework level
 | 
        
           |  |  | 80 |      * failure, but we can store them here to fail the step in i_look_for_exceptions()
 | 
        
           |  |  | 81 |      * which result will be parsed by the framework as the last step result.
 | 
        
           |  |  | 82 |      *
 | 
        
           |  |  | 83 |      * @var ?Exception Null or the exception last step throw in the before or after hook.
 | 
        
           |  |  | 84 |      */
 | 
        
           |  |  | 85 |     protected static $currentstepexception = null;
 | 
        
           |  |  | 86 |   | 
        
           |  |  | 87 |     /**
 | 
        
           |  |  | 88 |      * If an Exception is thrown in the BeforeScenario hook it will cause the Scenario to be skipped, and the exit code
 | 
        
           |  |  | 89 |      * to be non-zero triggering a potential rerun.
 | 
        
           |  |  | 90 |      *
 | 
        
           |  |  | 91 |      * To combat this the exception is stored and re-thrown when looking for exceptions.
 | 
        
           |  |  | 92 |      * This allows the test to instead be failed and re-run correctly.
 | 
        
           |  |  | 93 |      *
 | 
        
           |  |  | 94 |      * @var null|Exception
 | 
        
           |  |  | 95 |      */
 | 
        
           |  |  | 96 |     protected static $currentscenarioexception = null;
 | 
        
           |  |  | 97 |   | 
        
           |  |  | 98 |     /**
 | 
        
           |  |  | 99 |      * If we are saving any kind of dump on failure we should use the same parent dir during a run.
 | 
        
           |  |  | 100 |      *
 | 
        
           |  |  | 101 |      * @var The parent dir name
 | 
        
           |  |  | 102 |      */
 | 
        
           |  |  | 103 |     protected static $faildumpdirname = false;
 | 
        
           |  |  | 104 |   | 
        
           |  |  | 105 |     /**
 | 
        
           |  |  | 106 |      * Keeps track of time taken by feature to execute.
 | 
        
           |  |  | 107 |      *
 | 
        
           |  |  | 108 |      * @var array list of feature timings
 | 
        
           |  |  | 109 |      */
 | 
        
           |  |  | 110 |     protected static $timings = array();
 | 
        
           |  |  | 111 |   | 
        
           |  |  | 112 |     /**
 | 
        
           |  |  | 113 |      * Keeps track of current running suite name.
 | 
        
           |  |  | 114 |      *
 | 
        
           |  |  | 115 |      * @var string current running suite name
 | 
        
           |  |  | 116 |      */
 | 
        
           |  |  | 117 |     protected static $runningsuite = '';
 | 
        
           |  |  | 118 |   | 
        
           |  |  | 119 |     /**
 | 
        
           |  |  | 120 |      * @var array Array (with tag names in keys) of all tags in current scenario.
 | 
        
           |  |  | 121 |      */
 | 
        
           |  |  | 122 |     protected static $scenariotags;
 | 
        
           |  |  | 123 |   | 
        
           |  |  | 124 |     /**
 | 
        
           |  |  | 125 |      * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
 | 
        
           |  |  | 126 |      *
 | 
        
           |  |  | 127 |      * Includes config.php to use moodle codebase with $CFG->behat_* instead of $CFG->prefix and $CFG->dataroot, called
 | 
        
           |  |  | 128 |      * once per suite.
 | 
        
           |  |  | 129 |      *
 | 
        
           |  |  | 130 |      * @BeforeSuite
 | 
        
           |  |  | 131 |      * @param BeforeSuiteScope $scope scope passed by event fired before suite.
 | 
        
           |  |  | 132 |      */
 | 
        
           |  |  | 133 |     public static function before_suite_hook(BeforeSuiteScope $scope) {
 | 
        
           |  |  | 134 |         global $CFG;
 | 
        
           |  |  | 135 |   | 
        
           |  |  | 136 |         // If behat has been initialised then no need to do this again.
 | 
        
           |  |  | 137 |         if (!self::is_first_scenario()) {
 | 
        
           |  |  | 138 |             return;
 | 
        
           |  |  | 139 |         }
 | 
        
           |  |  | 140 |   | 
        
           |  |  | 141 |         // Defined only when the behat CLI command is running, the moodle init setup process will
 | 
        
           |  |  | 142 |         // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
 | 
        
           |  |  | 143 |         // the normal site.
 | 
        
           |  |  | 144 |         if (!defined('BEHAT_TEST')) {
 | 
        
           |  |  | 145 |             define('BEHAT_TEST', 1);
 | 
        
           |  |  | 146 |         }
 | 
        
           |  |  | 147 |   | 
        
           |  |  | 148 |         if (!defined('CLI_SCRIPT')) {
 | 
        
           |  |  | 149 |             define('CLI_SCRIPT', 1);
 | 
        
           |  |  | 150 |         }
 | 
        
           |  |  | 151 |   | 
        
           |  |  | 152 |         // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
 | 
        
           |  |  | 153 |         require_once(__DIR__ . '/../../../config.php');
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |         // Now that we are MOODLE_INTERNAL.
 | 
        
           |  |  | 156 |         require_once(__DIR__ . '/../../behat/classes/behat_command.php');
 | 
        
           |  |  | 157 |         require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
 | 
        
           |  |  | 158 |         require_once(__DIR__ . '/../../behat/classes/behat_context_helper.php');
 | 
        
           |  |  | 159 |         require_once(__DIR__ . '/../../behat/classes/util.php');
 | 
        
           |  |  | 160 |         require_once(__DIR__ . '/../../testing/classes/test_lock.php');
 | 
        
           |  |  | 161 |         require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 163 |         // Avoids vendor/bin/behat to be executed directly without test environment enabled
 | 
        
           |  |  | 164 |         // to prevent undesired db & dataroot modifications, this is also checked
 | 
        
           |  |  | 165 |         // before each scenario (accidental user deletes) in the BeforeScenario hook.
 | 
        
           |  |  | 166 |   | 
        
           |  |  | 167 |         if (!behat_util::is_test_mode_enabled()) {
 | 
        
           |  |  | 168 |             self::log_and_stop('Behat only can run if test mode is enabled. More info in ' .  behat_command::DOCS_URL);
 | 
        
           |  |  | 169 |         }
 | 
        
           |  |  | 170 |   | 
        
           |  |  | 171 |         // Reset all data, before checking for check_server_status.
 | 
        
           |  |  | 172 |         // If not done, then it can return apache error, while running tests.
 | 
        
           |  |  | 173 |         behat_util::clean_tables_updated_by_scenario_list();
 | 
        
           |  |  | 174 |         behat_util::reset_all_data();
 | 
        
           |  |  | 175 |   | 
        
           |  |  | 176 |         // Check if the web server is running and using same version for cli and apache.
 | 
        
           |  |  | 177 |         behat_util::check_server_status();
 | 
        
           |  |  | 178 |   | 
        
           |  |  | 179 |         // Prevents using outdated data, upgrade script would start and tests would fail.
 | 
        
           |  |  | 180 |         if (!behat_util::is_test_data_updated()) {
 | 
        
           |  |  | 181 |             $commandpath = 'php admin/tool/behat/cli/init.php';
 | 
        
           |  |  | 182 |             $message = <<<EOF
 | 
        
           |  |  | 183 | Your behat test site is outdated, please run the following command from your Moodle dirroot to drop, and reinstall the Behat test site.
 | 
        
           |  |  | 184 |   | 
        
           |  |  | 185 |     {$commandpath}
 | 
        
           |  |  | 186 |   | 
        
           |  |  | 187 | EOF;
 | 
        
           |  |  | 188 |             self::log_and_stop($message);
 | 
        
           |  |  | 189 |         }
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |         // Avoid parallel tests execution, it continues when the previous lock is released.
 | 
        
           |  |  | 192 |         test_lock::acquire('behat');
 | 
        
           |  |  | 193 |   | 
        
           |  |  | 194 |         if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
 | 
        
           |  |  | 195 |             self::log_and_stop(
 | 
        
           |  |  | 196 |                 "The \$CFG->behat_faildump_path value is set to a non-writable directory ({$CFG->behat_faildump_path})."
 | 
        
           |  |  | 197 |             );
 | 
        
           |  |  | 198 |         }
 | 
        
           |  |  | 199 |   | 
        
           |  |  | 200 |         // Handle interrupts on PHP7.
 | 
        
           |  |  | 201 |         if (extension_loaded('pcntl')) {
 | 
        
           |  |  | 202 |             $disabled = explode(',', ini_get('disable_functions'));
 | 
        
           |  |  | 203 |             if (!in_array('pcntl_signal', $disabled)) {
 | 
        
           |  |  | 204 |                 declare(ticks = 1);
 | 
        
           |  |  | 205 |             }
 | 
        
           |  |  | 206 |         }
 | 
        
           |  |  | 207 |     }
 | 
        
           |  |  | 208 |   | 
        
           |  |  | 209 |     /**
 | 
        
           |  |  | 210 |      * Run final tests before running the suite.
 | 
        
           |  |  | 211 |      *
 | 
        
           |  |  | 212 |      * @BeforeSuite
 | 
        
           |  |  | 213 |      * @param BeforeSuiteScope $scope scope passed by event fired before suite.
 | 
        
           |  |  | 214 |      */
 | 
        
           |  |  | 215 |     public static function before_suite_final_checks(BeforeSuiteScope $scope) {
 | 
        
           |  |  | 216 |         $happy = defined('BEHAT_TEST');
 | 
        
           |  |  | 217 |         $happy = $happy && defined('BEHAT_SITE_RUNNING');
 | 
        
           |  |  | 218 |         $happy = $happy && php_sapi_name() == 'cli';
 | 
        
           |  |  | 219 |         $happy = $happy && behat_util::is_test_mode_enabled();
 | 
        
           |  |  | 220 |         $happy = $happy && behat_util::is_test_site();
 | 
        
           |  |  | 221 |   | 
        
           |  |  | 222 |         if (!$happy) {
 | 
        
           |  |  | 223 |             error_log('Behat only can modify the test database and the test dataroot!');
 | 
        
           |  |  | 224 |             exit(1);
 | 
        
           |  |  | 225 |         }
 | 
        
           |  |  | 226 |     }
 | 
        
           |  |  | 227 |   | 
        
           |  |  | 228 |     /**
 | 
        
           |  |  | 229 |      * Gives access to moodle codebase, to keep track of feature start time.
 | 
        
           |  |  | 230 |      *
 | 
        
           |  |  | 231 |      * @param BeforeFeatureScope $scope scope passed by event fired before feature.
 | 
        
           |  |  | 232 |      * @BeforeFeature
 | 
        
           |  |  | 233 |      */
 | 
        
           |  |  | 234 |     public static function before_feature(BeforeFeatureScope $scope) {
 | 
        
           |  |  | 235 |         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
 | 
        
           |  |  | 236 |             return;
 | 
        
           |  |  | 237 |         }
 | 
        
           |  |  | 238 |         $file = $scope->getFeature()->getFile();
 | 
        
           |  |  | 239 |         self::$timings[$file] = microtime(true);
 | 
        
           |  |  | 240 |     }
 | 
        
           |  |  | 241 |   | 
        
           |  |  | 242 |     /**
 | 
        
           |  |  | 243 |      * Gives access to moodle codebase, to keep track of feature end time.
 | 
        
           |  |  | 244 |      *
 | 
        
           |  |  | 245 |      * @param AfterFeatureScope $scope scope passed by event fired after feature.
 | 
        
           |  |  | 246 |      * @AfterFeature
 | 
        
           |  |  | 247 |      */
 | 
        
           |  |  | 248 |     public static function after_feature(AfterFeatureScope $scope) {
 | 
        
           |  |  | 249 |         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
 | 
        
           |  |  | 250 |             return;
 | 
        
           |  |  | 251 |         }
 | 
        
           |  |  | 252 |         $file = $scope->getFeature()->getFile();
 | 
        
           |  |  | 253 |         self::$timings[$file] = microtime(true) - self::$timings[$file];
 | 
        
           |  |  | 254 |         // Probably didn't actually run this, don't output it.
 | 
        
           |  |  | 255 |         if (self::$timings[$file] < 1) {
 | 
        
           |  |  | 256 |             unset(self::$timings[$file]);
 | 
        
           |  |  | 257 |         }
 | 
        
           |  |  | 258 |     }
 | 
        
           |  |  | 259 |   | 
        
           |  |  | 260 |     /**
 | 
        
           |  |  | 261 |      * Gives access to moodle codebase, to keep track of suite timings.
 | 
        
           |  |  | 262 |      *
 | 
        
           |  |  | 263 |      * @param AfterSuiteScope $scope scope passed by event fired after suite.
 | 
        
           |  |  | 264 |      * @AfterSuite
 | 
        
           |  |  | 265 |      */
 | 
        
           |  |  | 266 |     public static function after_suite(AfterSuiteScope $scope) {
 | 
        
           |  |  | 267 |         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
 | 
        
           |  |  | 268 |             return;
 | 
        
           |  |  | 269 |         }
 | 
        
           |  |  | 270 |         $realroot = realpath(__DIR__.'/../../../').'/';
 | 
        
           |  |  | 271 |         foreach (self::$timings as $k => $v) {
 | 
        
           |  |  | 272 |             $new = str_replace($realroot, '', $k);
 | 
        
           |  |  | 273 |             self::$timings[$new] = round($v, 1);
 | 
        
           |  |  | 274 |             unset(self::$timings[$k]);
 | 
        
           |  |  | 275 |         }
 | 
        
           |  |  | 276 |         if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING_FILE), true)) {
 | 
        
           |  |  | 277 |             self::$timings = array_merge($existing, self::$timings);
 | 
        
           |  |  | 278 |         }
 | 
        
           |  |  | 279 |         arsort(self::$timings);
 | 
        
           |  |  | 280 |         @file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT));
 | 
        
           |  |  | 281 |     }
 | 
        
           |  |  | 282 |   | 
        
           |  |  | 283 |     /**
 | 
        
           |  |  | 284 |      * Helper function to restart the Mink session.
 | 
        
           |  |  | 285 |      */
 | 
        
           |  |  | 286 |     protected function restart_session(): void {
 | 
        
           |  |  | 287 |         $session = $this->getSession();
 | 
        
           |  |  | 288 |         if ($session->isStarted()) {
 | 
        
           |  |  | 289 |             $session->restart();
 | 
        
           |  |  | 290 |         } else {
 | 
        
           |  |  | 291 |             $this->start_session();
 | 
        
           |  |  | 292 |         }
 | 
        
           |  |  | 293 |         if ($this->running_javascript() && $this->getSession()->getDriver()->getWebDriverSessionId() === 'session') {
 | 
        
           |  |  | 294 |             throw new DriverException('Unable to create a valid session');
 | 
        
           |  |  | 295 |         }
 | 
        
           |  |  | 296 |     }
 | 
        
           |  |  | 297 |   | 
        
           |  |  | 298 |     /**
 | 
        
           |  |  | 299 |      * Start the Session, applying any initial configuratino required.
 | 
        
           |  |  | 300 |      */
 | 
        
           |  |  | 301 |     protected function start_session(): void {
 | 
        
           |  |  | 302 |         $this->getSession()->start();
 | 
        
           |  |  | 303 |   | 
        
           |  |  | 304 |         $this->set_test_timeout_factor(1);
 | 
        
           |  |  | 305 |     }
 | 
        
           |  |  | 306 |   | 
        
           |  |  | 307 |     /**
 | 
        
           |  |  | 308 |      * Restart the session before each non-javascript scenario.
 | 
        
           |  |  | 309 |      *
 | 
        
           |  |  | 310 |      * @BeforeScenario @~javascript
 | 
        
           |  |  | 311 |      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
 | 
        
           |  |  | 312 |      */
 | 
        
           |  |  | 313 |     public function before_browserkit_scenarios(BeforeScenarioScope $scope) {
 | 
        
           |  |  | 314 |         if ($this->running_javascript()) {
 | 
        
           |  |  | 315 |             // A bug in the BeforeScenario filtering prevents the @~javascript filter on this hook from working
 | 
        
           |  |  | 316 |             // properly.
 | 
        
           |  |  | 317 |             // See https://github.com/Behat/Behat/issues/1235 for further information.
 | 
        
           |  |  | 318 |             return;
 | 
        
           |  |  | 319 |         }
 | 
        
           |  |  | 320 |   | 
        
           |  |  | 321 |         $this->restart_session();
 | 
        
           |  |  | 322 |     }
 | 
        
           |  |  | 323 |   | 
        
           |  |  | 324 |     /**
 | 
        
           |  |  | 325 |      * Start the session before the first javascript scenario.
 | 
        
           |  |  | 326 |      *
 | 
        
           |  |  | 327 |      * This is treated slightly differently to try to capture when Selenium is not running at all.
 | 
        
           |  |  | 328 |      *
 | 
        
           |  |  | 329 |      * @BeforeScenario @javascript
 | 
        
           |  |  | 330 |      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
 | 
        
           |  |  | 331 |      */
 | 
        
           |  |  | 332 |     public function before_first_scenario_start_session(BeforeScenarioScope $scope) {
 | 
        
           |  |  | 333 |         if (!self::is_first_javascript_scenario()) {
 | 
        
           |  |  | 334 |             // The first Scenario has started.
 | 
        
           |  |  | 335 |             // The `before_subsequent_scenario_start_session` function will restart the session instead.
 | 
        
           |  |  | 336 |             return;
 | 
        
           |  |  | 337 |         }
 | 
        
           |  |  | 338 |   | 
        
           |  |  | 339 |         $docsurl = behat_command::DOCS_URL;
 | 
        
           |  |  | 340 |         $driverexceptionmsg = <<<EOF
 | 
        
           |  |  | 341 |   | 
        
           |  |  | 342 | The Selenium or WebDriver server is not running. You must start it to run tests that involve Javascript.
 | 
        
           |  |  | 343 | See {$docsurl} for more information.
 | 
        
           |  |  | 344 |   | 
        
           |  |  | 345 | The following debugging information is available:
 | 
        
           |  |  | 346 |   | 
        
           |  |  | 347 | EOF;
 | 
        
           |  |  | 348 |   | 
        
           |  |  | 349 |         try {
 | 
        
           |  |  | 350 |             $this->restart_session();
 | 
        
           |  |  | 351 |         } catch (WebDriverCurlException | DriverException $e) {
 | 
        
           |  |  | 352 |             // Thrown by WebDriver.
 | 
        
           |  |  | 353 |             self::log_and_stop(
 | 
        
           |  |  | 354 |                 $driverexceptionmsg . '. ' .
 | 
        
           |  |  | 355 |                 $e->getMessage() . "\n\n" .
 | 
        
           |  |  | 356 |                 format_backtrace($e->getTrace(), true)
 | 
        
           |  |  | 357 |             );
 | 
        
           |  |  | 358 |         } catch (UnknownErrorException $e) {
 | 
        
           |  |  | 359 |             // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
 | 
        
           |  |  | 360 |             self::log_and_stop(
 | 
        
           |  |  | 361 |                 $e->getMessage() . "\n\n" .
 | 
        
           |  |  | 362 |                 format_backtrace($e->getTrace(), true)
 | 
        
           |  |  | 363 |             );
 | 
        
           |  |  | 364 |         }
 | 
        
           |  |  | 365 |     }
 | 
        
           |  |  | 366 |   | 
        
           |  |  | 367 |     /**
 | 
        
           |  |  | 368 |      * Start the session before each javascript scenario.
 | 
        
           |  |  | 369 |      *
 | 
        
           |  |  | 370 |      * Note: Before the first scenario the @see before_first_scenario_start_session() function is used instead.
 | 
        
           |  |  | 371 |      *
 | 
        
           |  |  | 372 |      * @BeforeScenario @javascript
 | 
        
           |  |  | 373 |      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
 | 
        
           |  |  | 374 |      */
 | 
        
           |  |  | 375 |     public function before_subsequent_scenario_start_session(BeforeScenarioScope $scope) {
 | 
        
           |  |  | 376 |         if (self::is_first_javascript_scenario()) {
 | 
        
           |  |  | 377 |             // The initial init has not yet finished.
 | 
        
           |  |  | 378 |             // The `before_first_scenario_start_session` function will have started the session instead.
 | 
        
           |  |  | 379 |             return;
 | 
        
           |  |  | 380 |         }
 | 
        
           |  |  | 381 |         self::$currentscenarioexception = null;
 | 
        
           |  |  | 382 |   | 
        
           |  |  | 383 |         try {
 | 
        
           |  |  | 384 |             $this->restart_session();
 | 
        
           |  |  | 385 |         } catch (Exception $e) {
 | 
        
           |  |  | 386 |             self::$currentscenarioexception = $e;
 | 
        
           |  |  | 387 |         }
 | 
        
           |  |  | 388 |     }
 | 
        
           |  |  | 389 |   | 
        
           |  |  | 390 |     /**
 | 
        
           |  |  | 391 |      * Resets the test environment.
 | 
        
           |  |  | 392 |      *
 | 
        
           |  |  | 393 |      * @BeforeScenario
 | 
        
           |  |  | 394 |      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
 | 
        
           |  |  | 395 |      */
 | 
        
           |  |  | 396 |     public function before_scenario_hook(BeforeScenarioScope $scope) {
 | 
        
           |  |  | 397 |         global $DB;
 | 
        
           |  |  | 398 |         if (self::$currentscenarioexception) {
 | 
        
           |  |  | 399 |             // A BeforeScenario hook triggered an exception and marked this test as failed.
 | 
        
           |  |  | 400 |             // Skip this hook as it will likely fail.
 | 
        
           |  |  | 401 |             return;
 | 
        
           |  |  | 402 |         }
 | 
        
           |  |  | 403 |   | 
        
           |  |  | 404 |         $suitename = $scope->getSuite()->getName();
 | 
        
           |  |  | 405 |   | 
        
           |  |  | 406 |         // Register behat selectors for theme, if suite is changed. We do it for every suite change.
 | 
        
           |  |  | 407 |         if ($suitename !== self::$runningsuite) {
 | 
        
           |  |  | 408 |             self::$runningsuite = $suitename;
 | 
        
           |  |  | 409 |             behat_context_helper::set_environment($scope->getEnvironment());
 | 
        
           |  |  | 410 |   | 
        
           |  |  | 411 |             // We need the Mink session to do it and we do it only before the first scenario.
 | 
        
           |  |  | 412 |             $namedpartialclass = 'behat_partial_named_selector';
 | 
        
           |  |  | 413 |             $namedexactclass = 'behat_exact_named_selector';
 | 
        
           |  |  | 414 |   | 
        
           |  |  | 415 |             // If override selector exist, then set it as default behat selectors class.
 | 
        
           |  |  | 416 |             $overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_partial', true);
 | 
        
           |  |  | 417 |             if (class_exists($overrideclass)) {
 | 
        
           |  |  | 418 |                 $namedpartialclass = $overrideclass;
 | 
        
           |  |  | 419 |             }
 | 
        
           |  |  | 420 |   | 
        
           |  |  | 421 |             // If override selector exist, then set it as default behat selectors class.
 | 
        
           |  |  | 422 |             $overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_exact', true);
 | 
        
           |  |  | 423 |             if (class_exists($overrideclass)) {
 | 
        
           |  |  | 424 |                 $namedexactclass = $overrideclass;
 | 
        
           |  |  | 425 |             }
 | 
        
           |  |  | 426 |   | 
        
           |  |  | 427 |             $this->getSession()->getSelectorsHandler()->registerSelector('named_partial', new $namedpartialclass());
 | 
        
           |  |  | 428 |             $this->getSession()->getSelectorsHandler()->registerSelector('named_exact', new $namedexactclass());
 | 
        
           |  |  | 429 |   | 
        
           |  |  | 430 |             // Register component named selectors.
 | 
        
           |  |  | 431 |             foreach (\core_component::get_component_names() as $component) {
 | 
        
           |  |  | 432 |                 $this->register_component_selectors_for_component($component);
 | 
        
           |  |  | 433 |             }
 | 
        
           |  |  | 434 |   | 
        
           |  |  | 435 |         }
 | 
        
           |  |  | 436 |   | 
        
           |  |  | 437 |         // Reset $SESSION.
 | 
        
           |  |  | 438 |         \core\session\manager::init_empty_session();
 | 
        
           |  |  | 439 |   | 
        
           |  |  | 440 |         // Ignore E_NOTICE and E_WARNING during reset, as this might be caused because of some existing process
 | 
        
           |  |  | 441 |         // running ajax. This will be investigated in another issue.
 | 
        
           |  |  | 442 |         $errorlevel = error_reporting();
 | 
        
           |  |  | 443 |         error_reporting($errorlevel & ~E_NOTICE & ~E_WARNING);
 | 
        
           |  |  | 444 |         behat_util::reset_all_data();
 | 
        
           |  |  | 445 |         error_reporting($errorlevel);
 | 
        
           |  |  | 446 |   | 
        
           |  |  | 447 |         if ($this->running_javascript()) {
 | 
        
           |  |  | 448 |             // Fetch the user agent.
 | 
        
           |  |  | 449 |             // This isused to choose between the SVG/Non-SVG versions of themes.
 | 
        
           |  |  | 450 |             $useragent = $this->getSession()->evaluateScript('return navigator.userAgent;');
 | 
        
           |  |  | 451 |             \core_useragent::instance(true, $useragent);
 | 
        
           |  |  | 452 |   | 
        
           |  |  | 453 |             // Restore the saved themes.
 | 
        
           |  |  | 454 |             behat_util::restore_saved_themes();
 | 
        
           |  |  | 455 |         }
 | 
        
           |  |  | 456 |   | 
        
           |  |  | 457 |         // Assign valid data to admin user (some generator-related code needs a valid user).
 | 
        
           |  |  | 458 |         $user = $DB->get_record('user', array('username' => 'admin'));
 | 
        
           |  |  | 459 |         \core\session\manager::set_user($user);
 | 
        
           |  |  | 460 |   | 
        
           |  |  | 461 |         // Set the theme if not default.
 | 
        
           |  |  | 462 |         if ($suitename !== "default") {
 | 
        
           |  |  | 463 |             set_config('theme', $suitename);
 | 
        
           |  |  | 464 |         }
 | 
        
           |  |  | 465 |   | 
        
           |  |  | 466 |         // Reset the scenariorunning variable to ensure that Step 0 occurs.
 | 
        
           |  |  | 467 |         $this->scenariorunning = false;
 | 
        
           |  |  | 468 |   | 
        
           |  |  | 469 |         // Set up the tags for current scenario.
 | 
        
           |  |  | 470 |         self::fetch_tags_for_scenario($scope);
 | 
        
           |  |  | 471 |   | 
        
           |  |  | 472 |         // If scenario requires the Moodle app to be running, set this up.
 | 
        
           |  |  | 473 |         if ($this->has_tag('app')) {
 | 
        
           |  |  | 474 |             $this->execute('behat_app::start_scenario');
 | 
        
           |  |  | 475 |   | 
        
           |  |  | 476 |             return;
 | 
        
           |  |  | 477 |         }
 | 
        
           |  |  | 478 |   | 
        
           |  |  | 479 |         // Run all test with medium (1024x768) screen size, to avoid responsive problems.
 | 
        
           |  |  | 480 |         $this->resize_window('medium');
 | 
        
           |  |  | 481 |     }
 | 
        
           |  |  | 482 |   | 
        
           |  |  | 483 |     /**
 | 
        
           |  |  | 484 |      * Mark the first Javascript Scenario as have been seen.
 | 
        
           |  |  | 485 |      *
 | 
        
           |  |  | 486 |      * @BeforeScenario
 | 
        
           |  |  | 487 |      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
 | 
        
           |  |  | 488 |      */
 | 
        
           |  |  | 489 |     public function mark_first_js_scenario_as_seen(BeforeScenarioScope $scope) {
 | 
        
           |  |  | 490 |         self::$firstjavascriptscenarioseen = true;
 | 
        
           |  |  | 491 |     }
 | 
        
           |  |  | 492 |   | 
        
           |  |  | 493 |     /**
 | 
        
           |  |  | 494 |      * Hook to open the site root before the first step in the suite.
 | 
        
           |  |  | 495 |      * Yes, this is in a strange location and should be in the BeforeScenario hook, but failures in the test setUp lead
 | 
        
           |  |  | 496 |      * to the test being incorrectly marked as skipped with no way to force the test to be failed.
 | 
        
           |  |  | 497 |      *
 | 
        
           |  |  | 498 |      * @param BeforeStepScope $scope
 | 
        
           |  |  | 499 |      * @BeforeStep
 | 
        
           |  |  | 500 |      */
 | 
        
           |  |  | 501 |     public function before_step(BeforeStepScope $scope) {
 | 
        
           |  |  | 502 |         global $CFG;
 | 
        
           |  |  | 503 |   | 
        
           |  |  | 504 |         if (!$this->scenariorunning) {
 | 
        
           |  |  | 505 |             // We need to visit / before the first step in any Scenario.
 | 
        
           |  |  | 506 |             // This is our Step 0.
 | 
        
           |  |  | 507 |             // Ideally this would be in the BeforeScenario hook, but any exception in there will lead to the test being
 | 
        
           |  |  | 508 |             // skipped rather than it being failed.
 | 
        
           |  |  | 509 |             //
 | 
        
           |  |  | 510 |             // We also need to check that the site returned is a Behat site.
 | 
        
           |  |  | 511 |             // Again, this would be better in the BeforeSuite hook, but that does not have access to the selectors in
 | 
        
           |  |  | 512 |             // order to perform the necessary searches.
 | 
        
           |  |  | 513 |             $session = $this->getSession();
 | 
        
           |  |  | 514 |             $this->execute('behat_general::i_visit', ['/']);
 | 
        
           |  |  | 515 |   | 
        
           |  |  | 516 |             // Checking that the root path is a Moodle test site.
 | 
        
           |  |  | 517 |             if (self::is_first_scenario()) {
 | 
        
           |  |  | 518 |                 $message = "The base URL ({$CFG->wwwroot}) is not a behat test site. " .
 | 
        
           |  |  | 519 |                     'Ensure that you started the built-in web server in the correct directory, ' .
 | 
        
           |  |  | 520 |                     'or that your web server is correctly set up and started.';
 | 
        
           |  |  | 521 |   | 
        
           |  |  | 522 |                 $this->find(
 | 
        
           |  |  | 523 |                         "xpath", "//head/child::title[contains(., '" . behat_util::BEHATSITENAME . "')]",
 | 
        
           |  |  | 524 |                         new ExpectationException($message, $session)
 | 
        
           |  |  | 525 |                     );
 | 
        
           |  |  | 526 |   | 
        
           |  |  | 527 |             }
 | 
        
           |  |  | 528 |             $this->scenariorunning = true;
 | 
        
           |  |  | 529 |         }
 | 
        
           |  |  | 530 |     }
 | 
        
           |  |  | 531 |   | 
        
           |  |  | 532 |     /**
 | 
        
           |  |  | 533 |      * Sets up the tags for the current scenario.
 | 
        
           |  |  | 534 |      *
 | 
        
           |  |  | 535 |      * @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope Scope
 | 
        
           |  |  | 536 |      */
 | 
        
           |  |  | 537 |     protected static function fetch_tags_for_scenario(\Behat\Behat\Hook\Scope\BeforeScenarioScope $scope) {
 | 
        
           |  |  | 538 |         self::$scenariotags = array_flip(array_merge(
 | 
        
           |  |  | 539 |             $scope->getScenario()->getTags(),
 | 
        
           |  |  | 540 |             $scope->getFeature()->getTags()
 | 
        
           |  |  | 541 |         ));
 | 
        
           |  |  | 542 |     }
 | 
        
           |  |  | 543 |   | 
        
           |  |  | 544 |     /**
 | 
        
           |  |  | 545 |      * Gets the tags for the current scenario
 | 
        
           |  |  | 546 |      *
 | 
        
           |  |  | 547 |      * @return array Array where key is tag name and value is an integer
 | 
        
           |  |  | 548 |      */
 | 
        
           |  |  | 549 |     public static function get_tags_for_scenario(): array {
 | 
        
           |  |  | 550 |         return self::$scenariotags;
 | 
        
           |  |  | 551 |     }
 | 
        
           |  |  | 552 |   | 
        
           |  |  | 553 |     /**
 | 
        
           |  |  | 554 |      * Wait for JS to complete before beginning interacting with the DOM.
 | 
        
           |  |  | 555 |      *
 | 
        
           |  |  | 556 |      * Executed only when running against a real browser. We wrap it
 | 
        
           |  |  | 557 |      * all in a try & catch to forward the exception to i_look_for_exceptions
 | 
        
           |  |  | 558 |      * so the exception will be at scenario level, which causes a failure, by
 | 
        
           |  |  | 559 |      * default would be at framework level, which will stop the execution of
 | 
        
           |  |  | 560 |      * the run.
 | 
        
           |  |  | 561 |      *
 | 
        
           |  |  | 562 |      * @param BeforeStepScope $scope scope passed by event fired before step.
 | 
        
           |  |  | 563 |      * @BeforeStep
 | 
        
           |  |  | 564 |      */
 | 
        
           |  |  | 565 |     public function before_step_javascript(BeforeStepScope $scope) {
 | 
        
           |  |  | 566 |         if (self::$currentscenarioexception) {
 | 
        
           |  |  | 567 |             // A BeforeScenario hook triggered an exception and marked this test as failed.
 | 
        
           |  |  | 568 |             // Skip this hook as it will likely fail.
 | 
        
           |  |  | 569 |             return;
 | 
        
           |  |  | 570 |         }
 | 
        
           |  |  | 571 |   | 
        
           |  |  | 572 |         self::$currentstepexception = null;
 | 
        
           |  |  | 573 |   | 
        
           |  |  | 574 |         // Only run if JS.
 | 
        
           |  |  | 575 |         if ($this->running_javascript()) {
 | 
        
           |  |  | 576 |             try {
 | 
        
           |  |  | 577 |                 $this->wait_for_pending_js();
 | 
        
           |  |  | 578 |             } catch (Exception $e) {
 | 
        
           |  |  | 579 |                 self::$currentstepexception = $e;
 | 
        
           |  |  | 580 |             }
 | 
        
           |  |  | 581 |         }
 | 
        
           |  |  | 582 |     }
 | 
        
           |  |  | 583 |   | 
        
           |  |  | 584 |     /**
 | 
        
           |  |  | 585 |      * Wait for JS to complete after finishing the step.
 | 
        
           |  |  | 586 |      *
 | 
        
           |  |  | 587 |      * With this we ensure that there are not AJAX calls
 | 
        
           |  |  | 588 |      * still in progress.
 | 
        
           |  |  | 589 |      *
 | 
        
           |  |  | 590 |      * Executed only when running against a real browser. We wrap it
 | 
        
           |  |  | 591 |      * all in a try & catch to forward the exception to i_look_for_exceptions
 | 
        
           |  |  | 592 |      * so the exception will be at scenario level, which causes a failure, by
 | 
        
           |  |  | 593 |      * default would be at framework level, which will stop the execution of
 | 
        
           |  |  | 594 |      * the run.
 | 
        
           |  |  | 595 |      *
 | 
        
           |  |  | 596 |      * @param AfterStepScope $scope scope passed by event fired after step..
 | 
        
           |  |  | 597 |      * @AfterStep
 | 
        
           |  |  | 598 |      */
 | 
        
           |  |  | 599 |     public function after_step_javascript(AfterStepScope $scope) {
 | 
        
           |  |  | 600 |         global $CFG, $DB;
 | 
        
           |  |  | 601 |   | 
        
           |  |  | 602 |         // If step is undefined then throw exception, to get failed exit code.
 | 
        
           |  |  | 603 |         if ($scope->getTestResult()->getResultCode() === Behat\Behat\Tester\Result\StepResult::UNDEFINED) {
 | 
        
           |  |  | 604 |             throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined.");
 | 
        
           |  |  | 605 |         }
 | 
        
           |  |  | 606 |   | 
        
           |  |  | 607 |         $isfailed = $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED;
 | 
        
           |  |  | 608 |   | 
        
           |  |  | 609 |         // Abort any open transactions to prevent subsequent tests hanging.
 | 
        
           |  |  | 610 |         // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
 | 
        
           |  |  | 611 |         // want to see a message in the behat output.
 | 
        
           |  |  | 612 |         if (($scope->getTestResult() instanceof \Behat\Behat\Tester\Result\ExecutedStepResult) &&
 | 
        
           |  |  | 613 |             $scope->getTestResult()->hasException()) {
 | 
        
           |  |  | 614 |             if ($DB && $DB->is_transaction_started()) {
 | 
        
           |  |  | 615 |                 $DB->force_transaction_rollback();
 | 
        
           |  |  | 616 |             }
 | 
        
           |  |  | 617 |         }
 | 
        
           |  |  | 618 |   | 
        
           |  |  | 619 |         if ($isfailed && !empty($CFG->behat_faildump_path)) {
 | 
        
           |  |  | 620 |             // Save the page content (html).
 | 
        
           |  |  | 621 |             $this->take_contentdump($scope);
 | 
        
           |  |  | 622 |   | 
        
           |  |  | 623 |             if ($this->running_javascript()) {
 | 
        
           |  |  | 624 |                 // Save a screenshot.
 | 
        
           |  |  | 625 |                 $this->take_screenshot($scope);
 | 
        
           |  |  | 626 |             }
 | 
        
           |  |  | 627 |         }
 | 
        
           |  |  | 628 |   | 
        
           |  |  | 629 |         if ($isfailed && !empty($CFG->behat_pause_on_fail)) {
 | 
        
           |  |  | 630 |             $exception = $scope->getTestResult()->getException();
 | 
        
           |  |  | 631 |             $message = "<colour:lightRed>Scenario failed. ";
 | 
        
           |  |  | 632 |             $message .= "<colour:lightYellow>Paused for inspection. Press <colour:lightRed>Enter/Return<colour:lightYellow> to continue.<newline>";
 | 
        
           |  |  | 633 |             $message .= "<colour:lightRed>Exception follows:<newline>";
 | 
        
           |  |  | 634 |             $message .= trim($exception->getMessage());
 | 
        
           |  |  | 635 |             behat_util::pause($this->getSession(), $message);
 | 
        
           |  |  | 636 |         }
 | 
        
           |  |  | 637 |   | 
        
           |  |  | 638 |         // Only run if JS.
 | 
        
           |  |  | 639 |         if (!$this->running_javascript()) {
 | 
        
           |  |  | 640 |             return;
 | 
        
           |  |  | 641 |         }
 | 
        
           |  |  | 642 |   | 
        
           |  |  | 643 |         try {
 | 
        
           |  |  | 644 |             $this->wait_for_pending_js();
 | 
        
           |  |  | 645 |             self::$currentstepexception = null;
 | 
        
           |  |  | 646 |         } catch (UnexpectedAlertOpenException $e) {
 | 
        
           |  |  | 647 |             self::$currentstepexception = $e;
 | 
        
           |  |  | 648 |   | 
        
           |  |  | 649 |             // Accepting the alert so the framework can continue properly running
 | 
        
           |  |  | 650 |             // the following scenarios. Some browsers already closes the alert, so
 | 
        
           |  |  | 651 |             // wrapping in a try & catch.
 | 
        
           |  |  | 652 |             try {
 | 
        
           |  |  | 653 |                 $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert()->accept();
 | 
        
           |  |  | 654 |             } catch (Exception $e) {
 | 
        
           |  |  | 655 |                 // Catching the generic one as we never know how drivers reacts here.
 | 
        
           |  |  | 656 |             }
 | 
        
           |  |  | 657 |         } catch (Exception $e) {
 | 
        
           |  |  | 658 |             self::$currentstepexception = $e;
 | 
        
           |  |  | 659 |         }
 | 
        
           |  |  | 660 |     }
 | 
        
           |  |  | 661 |   | 
        
           |  |  | 662 |     /**
 | 
        
           |  |  | 663 |      * Reset the session between each scenario.
 | 
        
           |  |  | 664 |      *
 | 
        
           |  |  | 665 |      * @param AfterScenarioScope $scope scope passed by event fired after scenario.
 | 
        
           |  |  | 666 |      * @AfterScenario
 | 
        
           |  |  | 667 |      */
 | 
        
           |  |  | 668 |     public function reset_webdriver_between_scenarios(AfterScenarioScope $scope) {
 | 
        
           |  |  | 669 |         try {
 | 
        
           |  |  | 670 |             $this->getSession()->stop();
 | 
        
           |  |  | 671 |         } catch (Exception $e) {
 | 
        
           |  |  | 672 |             $error = <<<EOF
 | 
        
           |  |  | 673 |   | 
        
           |  |  | 674 | Error while stopping WebDriver: %s (%d) '%s'
 | 
        
           |  |  | 675 | Attempting to continue with test run. Stacktrace follows:
 | 
        
           |  |  | 676 |   | 
        
           |  |  | 677 | %s
 | 
        
           |  |  | 678 | EOF;
 | 
        
           |  |  | 679 |             error_log(sprintf(
 | 
        
           |  |  | 680 |                 $error,
 | 
        
           |  |  | 681 |                 get_class($e),
 | 
        
           |  |  | 682 |                 $e->getCode(),
 | 
        
           |  |  | 683 |                 $e->getMessage(),
 | 
        
           |  |  | 684 |                 format_backtrace($e->getTrace(), true)
 | 
        
           |  |  | 685 |             ));
 | 
        
           |  |  | 686 |         }
 | 
        
           |  |  | 687 |     }
 | 
        
           |  |  | 688 |   | 
        
           |  |  | 689 |     /**
 | 
        
           |  |  | 690 |      * Getter for self::$faildumpdirname
 | 
        
           |  |  | 691 |      *
 | 
        
           |  |  | 692 |      * @return string
 | 
        
           |  |  | 693 |      */
 | 
        
           |  |  | 694 |     protected function get_run_faildump_dir() {
 | 
        
           |  |  | 695 |         return self::$faildumpdirname;
 | 
        
           |  |  | 696 |     }
 | 
        
           |  |  | 697 |   | 
        
           |  |  | 698 |     /**
 | 
        
           |  |  | 699 |      * Take screenshot when a step fails.
 | 
        
           |  |  | 700 |      *
 | 
        
           |  |  | 701 |      * @throws Exception
 | 
        
           |  |  | 702 |      * @param AfterStepScope $scope scope passed by event after step.
 | 
        
           |  |  | 703 |      */
 | 
        
           |  |  | 704 |     protected function take_screenshot(AfterStepScope $scope) {
 | 
        
           |  |  | 705 |         // BrowserKit can't save screenshots.
 | 
        
           |  |  | 706 |         if (!$this->running_javascript()) {
 | 
        
           |  |  | 707 |             return false;
 | 
        
           |  |  | 708 |         }
 | 
        
           |  |  | 709 |   | 
        
           |  |  | 710 |         // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot.  If this isn't handled,
 | 
        
           |  |  | 711 |         // the behat run dies.  We don't want to lose the information about the failure that triggered the screenshot,
 | 
        
           |  |  | 712 |         // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
 | 
        
           |  |  | 713 |         // handling the failure as normal.
 | 
        
           |  |  | 714 |         try {
 | 
        
           |  |  | 715 |             list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
 | 
        
           |  |  | 716 |             $this->saveScreenshot($filename, $dir);
 | 
        
           |  |  | 717 |         } catch (Exception $e) {
 | 
        
           |  |  | 718 |             // Catching all exceptions as we don't know what the driver might throw.
 | 
        
           |  |  | 719 |             list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
 | 
        
           |  |  | 720 |             $message = "Could not save screenshot due to an error\n" . $e->getMessage();
 | 
        
           |  |  | 721 |             file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
 | 
        
           |  |  | 722 |         }
 | 
        
           |  |  | 723 |     }
 | 
        
           |  |  | 724 |   | 
        
           |  |  | 725 |     /**
 | 
        
           |  |  | 726 |      * Take a dump of the page content when a step fails.
 | 
        
           |  |  | 727 |      *
 | 
        
           |  |  | 728 |      * @throws Exception
 | 
        
           |  |  | 729 |      * @param AfterStepScope $scope scope passed by event after step.
 | 
        
           |  |  | 730 |      */
 | 
        
           |  |  | 731 |     protected function take_contentdump(AfterStepScope $scope) {
 | 
        
           |  |  | 732 |         list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
 | 
        
           |  |  | 733 |   | 
        
           |  |  | 734 |         try {
 | 
        
           |  |  | 735 |             // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
 | 
        
           |  |  | 736 |             $content = $this->getSession()->getPage()->getContent();
 | 
        
           |  |  | 737 |         } catch (Exception $e) {
 | 
        
           |  |  | 738 |             // Catching all exceptions as we don't know what the driver might throw.
 | 
        
           |  |  | 739 |             $content = "Could not save contentdump due to an error\n" . $e->getMessage();
 | 
        
           |  |  | 740 |         }
 | 
        
           |  |  | 741 |         file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
 | 
        
           |  |  | 742 |     }
 | 
        
           |  |  | 743 |   | 
        
           |  |  | 744 |     /**
 | 
        
           |  |  | 745 |      * Determine the full pathname to store a failure-related dump.
 | 
        
           |  |  | 746 |      *
 | 
        
           |  |  | 747 |      * This is used for content such as the DOM, and screenshots.
 | 
        
           |  |  | 748 |      *
 | 
        
           |  |  | 749 |      * @param AfterStepScope $scope scope passed by event after step.
 | 
        
           |  |  | 750 |      * @param String $filetype The file suffix to use. Limited to 4 chars.
 | 
        
           |  |  | 751 |      */
 | 
        
           |  |  | 752 |     protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
 | 
        
           |  |  | 753 |         global $CFG;
 | 
        
           |  |  | 754 |   | 
        
           |  |  | 755 |         // All the contentdumps should be in the same parent dir.
 | 
        
           |  |  | 756 |         if (!$faildumpdir = self::get_run_faildump_dir()) {
 | 
        
           |  |  | 757 |             $faildumpdir = self::$faildumpdirname = date('Ymd_His');
 | 
        
           |  |  | 758 |   | 
        
           |  |  | 759 |             $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
 | 
        
           |  |  | 760 |   | 
        
           |  |  | 761 |             if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) {
 | 
        
           |  |  | 762 |                 // It shouldn't, we already checked that the directory is writable.
 | 
        
           |  |  | 763 |                 throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.');
 | 
        
           |  |  | 764 |             }
 | 
        
           |  |  | 765 |         } else {
 | 
        
           |  |  | 766 |             // We will always need to know the full path.
 | 
        
           |  |  | 767 |             $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
 | 
        
           |  |  | 768 |         }
 | 
        
           |  |  | 769 |   | 
        
           |  |  | 770 |         // The scenario title + the failed step text.
 | 
        
           |  |  | 771 |         // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
 | 
        
           |  |  | 772 |         $filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText();
 | 
        
           |  |  | 773 |   | 
        
           |  |  | 774 |         // As file name is limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
 | 
        
           |  |  | 775 |         // extension as we allow .png for images and .html for DOM contents.
 | 
        
           |  |  | 776 |         $filenamelen = 245;
 | 
        
           |  |  | 777 |   | 
        
           |  |  | 778 |         // Suffix suite name to faildump file, if it's not default suite.
 | 
        
           |  |  | 779 |         $suitename = $scope->getSuite()->getName();
 | 
        
           |  |  | 780 |         if ($suitename != 'default') {
 | 
        
           |  |  | 781 |             $suitename = '_' . $suitename;
 | 
        
           |  |  | 782 |             $filenamelen = $filenamelen - strlen($suitename);
 | 
        
           |  |  | 783 |         } else {
 | 
        
           |  |  | 784 |             // No need to append suite name for default.
 | 
        
           |  |  | 785 |             $suitename = '';
 | 
        
           |  |  | 786 |         }
 | 
        
           |  |  | 787 |   | 
        
           |  |  | 788 |         $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
 | 
        
           |  |  | 789 |         $filename = substr($filename, 0, $filenamelen) . $suitename . '_' . $scope->getStep()->getLine() . '.' . $filetype;
 | 
        
           |  |  | 790 |   | 
        
           |  |  | 791 |         return array($dir, $filename);
 | 
        
           |  |  | 792 |     }
 | 
        
           |  |  | 793 |   | 
        
           |  |  | 794 |     /**
 | 
        
           |  |  | 795 |      * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
 | 
        
           |  |  | 796 |      *
 | 
        
           |  |  | 797 |      * Part of behat_hooks class as is part of the testing framework, is auto-executed
 | 
        
           |  |  | 798 |      * after each step so no features will splicitly use it.
 | 
        
           |  |  | 799 |      *
 | 
        
           |  |  | 800 |      * @Given /^I look for exceptions$/
 | 
        
           |  |  | 801 |      * @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception.
 | 
        
           |  |  | 802 |      * @see Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester
 | 
        
           |  |  | 803 |      */
 | 
        
           |  |  | 804 |     public function i_look_for_exceptions() {
 | 
        
           |  |  | 805 |         // If the scenario already failed in a hook throw the exception.
 | 
        
           |  |  | 806 |         if (!is_null(self::$currentscenarioexception)) {
 | 
        
           |  |  | 807 |             throw self::$currentscenarioexception;
 | 
        
           |  |  | 808 |         }
 | 
        
           |  |  | 809 |   | 
        
           |  |  | 810 |         // If the step already failed in a hook throw the exception.
 | 
        
           |  |  | 811 |         if (!is_null(self::$currentstepexception)) {
 | 
        
           |  |  | 812 |             throw self::$currentstepexception;
 | 
        
           |  |  | 813 |         }
 | 
        
           |  |  | 814 |   | 
        
           |  |  | 815 |         $this->look_for_exceptions();
 | 
        
           |  |  | 816 |     }
 | 
        
           |  |  | 817 |   | 
        
           |  |  | 818 |     /**
 | 
        
           |  |  | 819 |      * Returns whether the first scenario of the suite is running
 | 
        
           |  |  | 820 |      *
 | 
        
           |  |  | 821 |      * @return bool
 | 
        
           |  |  | 822 |      */
 | 
        
           |  |  | 823 |     protected static function is_first_scenario() {
 | 
        
           |  |  | 824 |         return !(self::$initprocessesfinished);
 | 
        
           |  |  | 825 |     }
 | 
        
           |  |  | 826 |   | 
        
           |  |  | 827 |     /**
 | 
        
           |  |  | 828 |      * Returns whether the first scenario of the suite is running
 | 
        
           |  |  | 829 |      *
 | 
        
           |  |  | 830 |      * @return bool
 | 
        
           |  |  | 831 |      */
 | 
        
           |  |  | 832 |     protected static function is_first_javascript_scenario(): bool {
 | 
        
           |  |  | 833 |         return !self::$firstjavascriptscenarioseen;
 | 
        
           |  |  | 834 |     }
 | 
        
           |  |  | 835 |   | 
        
           |  |  | 836 |     /**
 | 
        
           |  |  | 837 |      * Register a set of component selectors.
 | 
        
           |  |  | 838 |      *
 | 
        
           |  |  | 839 |      * @param string $component
 | 
        
           |  |  | 840 |      */
 | 
        
           |  |  | 841 |     public function register_component_selectors_for_component(string $component): void {
 | 
        
           |  |  | 842 |         $context = behat_context_helper::get_component_context($component);
 | 
        
           |  |  | 843 |   | 
        
           |  |  | 844 |         if ($context === null) {
 | 
        
           |  |  | 845 |             return;
 | 
        
           |  |  | 846 |         }
 | 
        
           |  |  | 847 |   | 
        
           |  |  | 848 |         $namedpartial = $this->getSession()->getSelectorsHandler()->getSelector('named_partial');
 | 
        
           |  |  | 849 |         $namedexact = $this->getSession()->getSelectorsHandler()->getSelector('named_exact');
 | 
        
           |  |  | 850 |   | 
        
           |  |  | 851 |         // Replacements must come before selectors as they are used in the selectors.
 | 
        
           |  |  | 852 |         foreach ($context->get_named_replacements() as $replacement) {
 | 
        
           |  |  | 853 |             $namedpartial->register_replacement($component, $replacement);
 | 
        
           |  |  | 854 |             $namedexact->register_replacement($component, $replacement);
 | 
        
           |  |  | 855 |         }
 | 
        
           |  |  | 856 |   | 
        
           |  |  | 857 |         foreach ($context->get_partial_named_selectors() as $selector) {
 | 
        
           |  |  | 858 |             $namedpartial->register_component_selector($component, $selector);
 | 
        
           |  |  | 859 |         }
 | 
        
           |  |  | 860 |   | 
        
           |  |  | 861 |         foreach ($context->get_exact_named_selectors() as $selector) {
 | 
        
           |  |  | 862 |             $namedexact->register_component_selector($component, $selector);
 | 
        
           |  |  | 863 |         }
 | 
        
           |  |  | 864 |   | 
        
           |  |  | 865 |     }
 | 
        
           |  |  | 866 |   | 
        
           |  |  | 867 |     /**
 | 
        
           |  |  | 868 |      * Mark the first step as having been completed.
 | 
        
           |  |  | 869 |      *
 | 
        
           |  |  | 870 |      * This must be the last BeforeStep hook in the setup.
 | 
        
           |  |  | 871 |      *
 | 
        
           |  |  | 872 |      * @param BeforeStepScope $scope
 | 
        
           |  |  | 873 |      * @BeforeStep
 | 
        
           |  |  | 874 |      */
 | 
        
           |  |  | 875 |     public function first_step_setup_complete(BeforeStepScope $scope): void {
 | 
        
           |  |  | 876 |         self::$initprocessesfinished = true;
 | 
        
           |  |  | 877 |     }
 | 
        
           |  |  | 878 |   | 
        
           |  |  | 879 |     /**
 | 
        
           |  |  | 880 |      * Log a notification, and then exit.
 | 
        
           |  |  | 881 |      *
 | 
        
           |  |  | 882 |      * @param   string $message The content to dispaly
 | 
        
           |  |  | 883 |      */
 | 
        
           |  |  | 884 |     protected static function log_and_stop(string $message): void {
 | 
        
           |  |  | 885 |         error_log($message);
 | 
        
           |  |  | 886 |   | 
        
           |  |  | 887 |         exit(1);
 | 
        
           |  |  | 888 |     }
 | 
        
           |  |  | 889 |   | 
        
           |  |  | 890 | }
 |