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