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