Commit | Line | Data |
---|---|---|
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 | ||
30 | require_once(__DIR__ . '/../../behat/behat_base.php'); | |
31 | ||
1303eb29 DM |
32 | use Behat\Behat\Event\SuiteEvent as SuiteEvent, |
33 | Behat\Behat\Event\ScenarioEvent as ScenarioEvent, | |
34 | Behat\Behat\Event\StepEvent as StepEvent, | |
89cf999a | 35 | Behat\Mink\Exception\DriverException as DriverException, |
870349ee DM |
36 | WebDriver\Exception\NoSuchWindow as NoSuchWindow, |
37 | WebDriver\Exception\UnexpectedAlertOpen as UnexpectedAlertOpen, | |
3b7d3fb8 DM |
38 | WebDriver\Exception\UnknownError as UnknownError, |
39 | WebDriver\Exception\CurlExec as CurlExec, | |
870349ee | 40 | WebDriver\Exception\NoAlertOpenError as NoAlertOpenError; |
f5ceb6c2 DM |
41 | |
42 | /** | |
43 | * Hooks to the behat process. | |
44 | * | |
45 | * Behat accepts hooks after and before each | |
46 | * suite, feature, scenario and step. | |
47 | * | |
48 | * They can not call other steps as part of their process | |
49 | * like regular steps definitions does. | |
50 | * | |
51 | * Throws generic Exception because they are captured by Behat. | |
52 | * | |
53 | * @package core | |
54 | * @category test | |
55 | * @copyright 2012 David Monllaó | |
56 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
57 | */ | |
58 | class behat_hooks extends behat_base { | |
59 | ||
41eb672b DM |
60 | /** |
61 | * @var Last browser session start time. | |
62 | */ | |
63 | protected static $lastbrowsersessionstart = 0; | |
64 | ||
95b43d6b DM |
65 | /** |
66 | * @var For actions that should only run once. | |
67 | */ | |
68 | protected static $initprocessesfinished = false; | |
69 | ||
f49cede7 DM |
70 | /** |
71 | * Some exceptions can only be caught in a before or after step hook, | |
72 | * they can not be thrown there as they will provoke a framework level | |
73 | * failure, but we can store them here to fail the step in i_look_for_exceptions() | |
74 | * which result will be parsed by the framework as the last step result. | |
75 | * | |
76 | * @var Null or the exception last step throw in the before or after hook. | |
77 | */ | |
78 | protected static $currentstepexception = null; | |
79 | ||
5c0dfe32 | 80 | /** |
a964ead0 | 81 | * If we are saving any kind of dump on failure we should use the same parent dir during a run. |
5c0dfe32 DM |
82 | * |
83 | * @var The parent dir name | |
84 | */ | |
a964ead0 | 85 | protected static $faildumpdirname = false; |
5c0dfe32 | 86 | |
f5ceb6c2 DM |
87 | /** |
88 | * Gives access to moodle codebase, ensures all is ready and sets up the test lock. | |
89 | * | |
90 | * Includes config.php to use moodle codebase with $CFG->behat_* | |
91 | * instead of $CFG->prefix and $CFG->dataroot, called once per suite. | |
92 | * | |
93 | * @static | |
94 | * @throws Exception | |
95 | * @BeforeSuite | |
96 | */ | |
97 | public static function before_suite($event) { | |
98 | global $CFG; | |
99 | ||
cfcbc34a DM |
100 | // Defined only when the behat CLI command is running, the moodle init setup process will |
101 | // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of | |
102 | // the normal site. | |
103 | define('BEHAT_TEST', 1); | |
104 | ||
f5ceb6c2 DM |
105 | define('CLI_SCRIPT', 1); |
106 | ||
cfcbc34a | 107 | // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot. |
f5ceb6c2 DM |
108 | require_once(__DIR__ . '/../../../config.php'); |
109 | ||
110 | // Now that we are MOODLE_INTERNAL. | |
111 | require_once(__DIR__ . '/../../behat/classes/behat_command.php'); | |
17344d4c | 112 | require_once(__DIR__ . '/../../behat/classes/behat_selectors.php'); |
af4830a2 | 113 | require_once(__DIR__ . '/../../behat/classes/behat_context_helper.php'); |
f5ceb6c2 DM |
114 | require_once(__DIR__ . '/../../behat/classes/util.php'); |
115 | require_once(__DIR__ . '/../../testing/classes/test_lock.php'); | |
b08b0a28 | 116 | require_once(__DIR__ . '/../../testing/classes/nasty_strings.php'); |
f5ceb6c2 DM |
117 | |
118 | // Avoids vendor/bin/behat to be executed directly without test environment enabled | |
119 | // to prevent undesired db & dataroot modifications, this is also checked | |
120 | // before each scenario (accidental user deletes) in the BeforeScenario hook. | |
121 | ||
122 | if (!behat_util::is_test_mode_enabled()) { | |
123 | throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests'); | |
124 | } | |
125 | ||
ab25d8a7 RT |
126 | // Reset all data, before checking for is_server_running. |
127 | // If not done, then it can return apache error, while running tests. | |
128 | behat_util::reset_all_data(); | |
129 | ||
f5ceb6c2 | 130 | if (!behat_util::is_server_running()) { |
3b7d3fb8 | 131 | throw new Exception($CFG->behat_wwwroot . |
60129d5d | 132 | ' is not available, ensure you specified correct url and that the server is set up and started.' . |
3b7d3fb8 | 133 | ' More info in ' . behat_command::DOCS_URL . '#Running_tests'); |
f5ceb6c2 DM |
134 | } |
135 | ||
b831d479 DM |
136 | // Prevents using outdated data, upgrade script would start and tests would fail. |
137 | if (!behat_util::is_test_data_updated()) { | |
b32ca4ca | 138 | $commandpath = 'php admin/tool/behat/cli/init.php'; |
d69a6ad9 TH |
139 | throw new Exception("Your behat test site is outdated, please run\n\n " . |
140 | $commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again."); | |
b831d479 | 141 | } |
f5ceb6c2 DM |
142 | // Avoid parallel tests execution, it continues when the previous lock is released. |
143 | test_lock::acquire('behat'); | |
41eb672b DM |
144 | |
145 | // Store the browser reset time if reset after N seconds is specified in config.php. | |
146 | if (!empty($CFG->behat_restart_browser_after)) { | |
147 | // Store the initial browser session opening. | |
148 | self::$lastbrowsersessionstart = time(); | |
149 | } | |
5c0dfe32 | 150 | |
a964ead0 AN |
151 | if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) { |
152 | throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory'); | |
5c0dfe32 | 153 | } |
f5ceb6c2 DM |
154 | } |
155 | ||
156 | /** | |
157 | * Resets the test environment. | |
158 | * | |
159 | * @throws coding_exception If here we are not using the test database it should be because of a coding error | |
160 | * @BeforeScenario | |
161 | */ | |
162 | public function before_scenario($event) { | |
163 | global $DB, $SESSION, $CFG; | |
164 | ||
165 | // As many checks as we can. | |
cfcbc34a DM |
166 | if (!defined('BEHAT_TEST') || |
167 | !defined('BEHAT_SITE_RUNNING') || | |
f5ceb6c2 DM |
168 | php_sapi_name() != 'cli' || |
169 | !behat_util::is_test_mode_enabled() || | |
cfcbc34a | 170 | !behat_util::is_test_site()) { |
46ac40cd | 171 | throw new coding_exception('Behat only can modify the test database and the test dataroot!'); |
f5ceb6c2 DM |
172 | } |
173 | ||
89cf999a DM |
174 | $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests'; |
175 | $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo; | |
f59f6b6b DM |
176 | try { |
177 | $session = $this->getSession(); | |
178 | } catch (CurlExec $e) { | |
179 | // Exception thrown by WebDriver, so only @javascript tests will be caugth; in | |
180 | // behat_util::is_server_running() we already checked that the server is running. | |
89cf999a DM |
181 | throw new Exception($driverexceptionmsg); |
182 | } catch (DriverException $e) { | |
183 | throw new Exception($driverexceptionmsg); | |
f59f6b6b DM |
184 | } catch (UnknownError $e) { |
185 | // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions. | |
186 | $this->throw_unknown_exception($e); | |
187 | } | |
188 | ||
189 | ||
17344d4c DM |
190 | // We need the Mink session to do it and we do it only before the first scenario. |
191 | if (self::is_first_scenario()) { | |
f59f6b6b | 192 | behat_selectors::register_moodle_selectors($session); |
af4830a2 | 193 | behat_context_helper::set_session($session); |
17344d4c DM |
194 | } |
195 | ||
de230fd3 | 196 | // Reset $SESSION. |
2e00d01d | 197 | \core\session\manager::init_empty_session(); |
c8619f33 | 198 | |
ef1d45b3 | 199 | behat_util::reset_all_data(); |
b08b0a28 | 200 | |
de230fd3 | 201 | // Assign valid data to admin user (some generator-related code needs a valid user). |
f5ceb6c2 | 202 | $user = $DB->get_record('user', array('username' => 'admin')); |
d79d5ac2 | 203 | \core\session\manager::set_user($user); |
5f4b4e91 | 204 | |
41eb672b DM |
205 | // Reset the browser if specified in config.php. |
206 | if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) { | |
207 | $now = time(); | |
208 | if (self::$lastbrowsersessionstart + $CFG->behat_restart_browser_after < $now) { | |
f59f6b6b | 209 | $session->restart(); |
41eb672b DM |
210 | self::$lastbrowsersessionstart = $now; |
211 | } | |
212 | } | |
213 | ||
5f4b4e91 | 214 | // Start always in the the homepage. |
3b7d3fb8 | 215 | try { |
f59f6b6b DM |
216 | // Let's be conservative as we never know when new upstream issues will affect us. |
217 | $session->visit($this->locate_path('/')); | |
3b7d3fb8 | 218 | } catch (UnknownError $e) { |
3b7d3fb8 DM |
219 | $this->throw_unknown_exception($e); |
220 | } | |
870349ee | 221 | |
f59f6b6b | 222 | |
95b43d6b DM |
223 | // Checking that the root path is a Moodle test site. |
224 | if (self::is_first_scenario()) { | |
225 | $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' . | |
3b7d3fb8 | 226 | 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up'); |
19f6703d | 227 | $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception); |
95b43d6b DM |
228 | |
229 | self::$initprocessesfinished = true; | |
230 | } | |
3b0b5e57 RT |
231 | // Run all test with medium (1024x768) screen size, to avoid responsive problems. |
232 | $this->resize_window('medium'); | |
f5ceb6c2 DM |
233 | } |
234 | ||
f5ceb6c2 | 235 | /** |
d1e55a47 | 236 | * Wait for JS to complete before beginning interacting with the DOM. |
f5ceb6c2 | 237 | * |
f49cede7 DM |
238 | * Executed only when running against a real browser. We wrap it |
239 | * all in a try & catch to forward the exception to i_look_for_exceptions | |
240 | * so the exception will be at scenario level, which causes a failure, by | |
241 | * default would be at framework level, which will stop the execution of | |
242 | * the run. | |
f5ceb6c2 | 243 | * |
6e2c417c | 244 | * @BeforeStep @javascript |
f5ceb6c2 | 245 | */ |
6e2c417c | 246 | public function before_step_javascript($event) { |
f49cede7 DM |
247 | |
248 | try { | |
249 | $this->wait_for_pending_js(); | |
250 | self::$currentstepexception = null; | |
251 | } catch (Exception $e) { | |
252 | self::$currentstepexception = $e; | |
253 | } | |
d1e55a47 DM |
254 | } |
255 | ||
256 | /** | |
257 | * Wait for JS to complete after finishing the step. | |
258 | * | |
259 | * With this we ensure that there are not AJAX calls | |
260 | * still in progress. | |
261 | * | |
f49cede7 DM |
262 | * Executed only when running against a real browser. We wrap it |
263 | * all in a try & catch to forward the exception to i_look_for_exceptions | |
264 | * so the exception will be at scenario level, which causes a failure, by | |
265 | * default would be at framework level, which will stop the execution of | |
266 | * the run. | |
d1e55a47 DM |
267 | * |
268 | * @AfterStep @javascript | |
269 | */ | |
270 | public function after_step_javascript($event) { | |
5c0dfe32 DM |
271 | global $CFG; |
272 | ||
273 | // Save a screenshot if the step failed. | |
a964ead0 | 274 | if (!empty($CFG->behat_faildump_path) && |
5c0dfe32 DM |
275 | $event->getResult() === StepEvent::FAILED) { |
276 | $this->take_screenshot($event); | |
277 | } | |
f49cede7 DM |
278 | |
279 | try { | |
280 | $this->wait_for_pending_js(); | |
281 | self::$currentstepexception = null; | |
282 | } catch (UnexpectedAlertOpen $e) { | |
283 | self::$currentstepexception = $e; | |
284 | ||
285 | // Accepting the alert so the framework can continue properly running | |
286 | // the following scenarios. Some browsers already closes the alert, so | |
287 | // wrapping in a try & catch. | |
288 | try { | |
289 | $this->getSession()->getDriver()->getWebDriverSession()->accept_alert(); | |
290 | } catch (Exception $e) { | |
291 | // Catching the generic one as we never know how drivers reacts here. | |
292 | } | |
293 | } catch (Exception $e) { | |
294 | self::$currentstepexception = $e; | |
295 | } | |
d1e55a47 DM |
296 | } |
297 | ||
3ec07614 | 298 | /** |
a964ead0 AN |
299 | * Execute any steps required after the step has finished. |
300 | * | |
301 | * This includes creating an HTML dump of the content if there was a failure. | |
302 | * | |
303 | * @AfterStep | |
304 | */ | |
305 | public function after_step($event) { | |
306 | global $CFG; | |
307 | ||
308 | // Save the page content if the step failed. | |
309 | if (!empty($CFG->behat_faildump_path) && | |
310 | $event->getResult() === StepEvent::FAILED) { | |
311 | $this->take_contentdump($event); | |
312 | } | |
313 | } | |
314 | ||
315 | /** | |
316 | * Getter for self::$faildumpdirname | |
5c0dfe32 DM |
317 | * |
318 | * @return string | |
319 | */ | |
a964ead0 AN |
320 | protected function get_run_faildump_dir() { |
321 | return self::$faildumpdirname; | |
5c0dfe32 DM |
322 | } |
323 | ||
324 | /** | |
325 | * Take screenshot when a step fails. | |
3ec07614 | 326 | * |
5c0dfe32 DM |
327 | * @throws Exception |
328 | * @param StepEvent $event | |
3ec07614 | 329 | */ |
5c0dfe32 | 330 | protected function take_screenshot(StepEvent $event) { |
5c0dfe32 DM |
331 | // Goutte can't save screenshots. |
332 | if (!$this->running_javascript()) { | |
333 | return false; | |
3ec07614 | 334 | } |
5c0dfe32 | 335 | |
a964ead0 AN |
336 | list ($dir, $filename) = $this->get_faildump_filename($event, 'png'); |
337 | $this->saveScreenshot($filename, $dir); | |
338 | } | |
339 | ||
340 | /** | |
341 | * Take a dump of the page content when a step fails. | |
342 | * | |
343 | * @throws Exception | |
344 | * @param StepEvent $event | |
345 | */ | |
346 | protected function take_contentdump(StepEvent $event) { | |
347 | list ($dir, $filename) = $this->get_faildump_filename($event, 'html'); | |
348 | ||
349 | $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w'); | |
350 | fwrite($fh, $this->getSession()->getPage()->getContent()); | |
351 | fclose($fh); | |
352 | } | |
353 | ||
354 | /** | |
355 | * Determine the full pathname to store a failure-related dump. | |
356 | * | |
357 | * This is used for content such as the DOM, and screenshots. | |
358 | * | |
359 | * @param StepEvent $event | |
167486b3 | 360 | * @param String $filetype The file suffix to use. Limited to 4 chars. |
a964ead0 AN |
361 | */ |
362 | protected function get_faildump_filename(StepEvent $event, $filetype) { | |
363 | global $CFG; | |
364 | ||
365 | // All the contentdumps should be in the same parent dir. | |
366 | if (!$faildumpdir = self::get_run_faildump_dir()) { | |
367 | $faildumpdir = self::$faildumpdirname = date('Ymd_His'); | |
5c0dfe32 | 368 | |
a964ead0 | 369 | $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir; |
5c0dfe32 | 370 | |
a964ead0 | 371 | if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) { |
5c0dfe32 | 372 | // It shouldn't, we already checked that the directory is writable. |
a964ead0 | 373 | throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.'); |
5c0dfe32 DM |
374 | } |
375 | } else { | |
376 | // We will always need to know the full path. | |
a964ead0 | 377 | $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir; |
5c0dfe32 DM |
378 | } |
379 | ||
380 | // The scenario title + the failed step text. | |
a964ead0 | 381 | // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format. |
5c0dfe32 | 382 | $filename = $event->getStep()->getParent()->getTitle() . '_' . $event->getStep()->getText(); |
167486b3 DM |
383 | $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename); |
384 | ||
a2c271dd | 385 | // File name limited to 255 characters. Leaving 4 chars for the file |
167486b3 | 386 | // extension as we allow .png for images and .html for DOM contents. |
a2c271dd | 387 | $filename = substr($filename, 0, 250) . '.' . $filetype; |
5c0dfe32 | 388 | |
a964ead0 | 389 | return array($dir, $filename); |
3ec07614 DW |
390 | } |
391 | ||
d1e55a47 DM |
392 | /** |
393 | * Waits for all the JS to be loaded. | |
394 | * | |
afe9f42a | 395 | * @throws \Exception |
d1e55a47 DM |
396 | * @throws NoSuchWindow |
397 | * @throws UnknownError | |
398 | * @return bool True or false depending whether all the JS is loaded or not. | |
399 | */ | |
400 | protected function wait_for_pending_js() { | |
401 | ||
c1faf86b DM |
402 | // We don't use behat_base::spin() here as we don't want to end up with an exception |
403 | // if the page & JSs don't finish loading properly. | |
404 | for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) { | |
6e2c417c DW |
405 | $pending = ''; |
406 | try { | |
d1e55a47 DM |
407 | $jscode = 'return ' . self::PAGE_READY_JS . ' ? "" : M.util.pending_js.join(":");'; |
408 | $pending = $this->getSession()->evaluateScript($jscode); | |
6e2c417c | 409 | } catch (NoSuchWindow $nsw) { |
d1e55a47 | 410 | // We catch an exception here, in case we just closed the window we were interacting with. |
6e2c417c DW |
411 | // No javascript is running if there is no window right? |
412 | $pending = ''; | |
d1e55a47 | 413 | } catch (UnknownError $e) { |
f49cede7 DM |
414 | // M is not defined when the window or the frame don't exist anymore. |
415 | if (strstr($e->getMessage(), 'M is not defined') != false) { | |
416 | $pending = ''; | |
417 | } | |
28abad1a | 418 | } |
d1e55a47 DM |
419 | |
420 | // If there are no pending JS we stop waiting. | |
6e2c417c | 421 | if ($pending === '') { |
d1e55a47 | 422 | return true; |
6e2c417c | 423 | } |
d1e55a47 | 424 | |
6e2c417c DW |
425 | // 0.1 seconds. |
426 | usleep(100000); | |
1303eb29 | 427 | } |
d1e55a47 | 428 | |
afe9f42a DM |
429 | // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions(). |
430 | // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds | |
431 | // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the | |
432 | // number of JS pending code and JS completed code will not match and we will reach this point. | |
433 | throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT . | |
434 | ' seconds. There is a Javascript error or the code is extremely slow.'); | |
f5ceb6c2 DM |
435 | } |
436 | ||
5f4b4e91 DM |
437 | /** |
438 | * Internal step definition to find exceptions, debugging() messages and PHP debug messages. | |
439 | * | |
440 | * Part of behat_hooks class as is part of the testing framework, is auto-executed | |
441 | * after each step so no features will splicitly use it. | |
442 | * | |
443 | * @Given /^I look for exceptions$/ | |
f49cede7 | 444 | * @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception. |
5f4b4e91 DM |
445 | * @see Moodle\BehatExtension\Tester\MoodleStepTester |
446 | */ | |
447 | public function i_look_for_exceptions() { | |
448 | ||
f49cede7 DM |
449 | // If the step already failed in a hook throw the exception. |
450 | if (!is_null(self::$currentstepexception)) { | |
451 | throw self::$currentstepexception; | |
452 | } | |
453 | ||
217e8e59 DM |
454 | // Wrap in try in case we were interacting with a closed window. |
455 | try { | |
5f4b4e91 | 456 | |
217e8e59 | 457 | // Exceptions. |
3e76c7fa | 458 | $exceptionsxpath = "//div[@data-rel='fatalerror']"; |
8aea365f | 459 | // Debugging messages. |
3e76c7fa | 460 | $debuggingxpath = "//div[@data-rel='debugging']"; |
8aea365f | 461 | // PHP debug messages. |
3e76c7fa | 462 | $phperrorxpath = "//div[@data-rel='phpdebugmessage']"; |
8aea365f | 463 | // Any other backtrace. |
6cd21356 | 464 | $othersxpath = "(//*[contains(., ': call to ')])[1]"; |
8aea365f DM |
465 | |
466 | $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath); | |
467 | $joinedxpath = implode(' | ', $xpaths); | |
468 | ||
469 | // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check | |
470 | // is faster than to send the 4 xpath queries for each step. | |
471 | if (!$this->getSession()->getDriver()->find($joinedxpath)) { | |
472 | return; | |
473 | } | |
474 | ||
475 | // Exceptions. | |
476 | if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { | |
5f4b4e91 | 477 | |
217e8e59 | 478 | // Getting the debugging info and the backtrace. |
f8b589c9 RT |
479 | $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); |
480 | // If errorinfoboxes is empty, try find notifytiny (original) class. | |
481 | if (empty($errorinfoboxes)) { | |
482 | $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); | |
483 | } | |
217e8e59 DM |
484 | $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . |
485 | $this->get_debug_text($errorinfoboxes[1]->getHtml()); | |
5f4b4e91 | 486 | |
217e8e59 DM |
487 | $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; |
488 | throw new \Exception(html_entity_decode($msg)); | |
489 | } | |
490 | ||
491 | // Debugging messages. | |
8aea365f | 492 | if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { |
217e8e59 DM |
493 | $msgs = array(); |
494 | foreach ($debuggingmessages as $debuggingmessage) { | |
495 | $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); | |
496 | } | |
497 | $msg = "debugging() message/s found:\n" . implode("\n", $msgs); | |
498 | throw new \Exception(html_entity_decode($msg)); | |
5f4b4e91 | 499 | } |
90ed22ab | 500 | |
217e8e59 | 501 | // PHP debug messages. |
8aea365f | 502 | if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { |
90ed22ab | 503 | |
217e8e59 DM |
504 | $msgs = array(); |
505 | foreach ($phpmessages as $phpmessage) { | |
506 | $msgs[] = $this->get_debug_text($phpmessage->getHtml()); | |
507 | } | |
508 | $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); | |
509 | throw new \Exception(html_entity_decode($msg)); | |
90ed22ab | 510 | } |
bbd802f7 | 511 | |
217e8e59 | 512 | // Any other backtrace. |
9a1f4922 DM |
513 | // First looking through xpath as it is faster than get and parse the whole page contents, |
514 | // we get the contents and look for matches once we found something to suspect that there is a backtrace. | |
8aea365f | 515 | if ($this->getSession()->getDriver()->find($othersxpath)) { |
9a1f4922 DM |
516 | $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/'; |
517 | if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) { | |
518 | $msgs = array(); | |
519 | foreach ($backtraces[0] as $backtrace) { | |
520 | $msgs[] = $backtrace . '()'; | |
521 | } | |
522 | $msg = "Other backtraces found:\n" . implode("\n", $msgs); | |
523 | throw new \Exception(htmlentities($msg)); | |
217e8e59 | 524 | } |
bbd802f7 | 525 | } |
217e8e59 DM |
526 | |
527 | } catch (NoSuchWindow $e) { | |
528 | // If we were interacting with a popup window it will not exists after closing it. | |
bbd802f7 | 529 | } |
5f4b4e91 DM |
530 | } |
531 | ||
532 | /** | |
533 | * Converts HTML tags to line breaks to display the info in CLI | |
534 | * | |
535 | * @param string $html | |
536 | * @return string | |
537 | */ | |
538 | protected function get_debug_text($html) { | |
539 | ||
540 | // Replacing HTML tags for new lines and keeping only the text. | |
541 | $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html); | |
542 | return preg_replace("/(\n)+/s", "\n", $notags); | |
543 | } | |
544 | ||
95b43d6b DM |
545 | /** |
546 | * Returns whether the first scenario of the suite is running | |
547 | * | |
548 | * @return bool | |
549 | */ | |
550 | protected static function is_first_scenario() { | |
551 | return !(self::$initprocessesfinished); | |
552 | } | |
3b7d3fb8 DM |
553 | |
554 | /** | |
555 | * Throws an exception after appending an extra info text. | |
556 | * | |
557 | * @throws Exception | |
558 | * @param UnknownError $exception | |
559 | * @return void | |
560 | */ | |
561 | protected function throw_unknown_exception(UnknownError $exception) { | |
562 | $text = get_string('unknownexceptioninfo', 'tool_behat'); | |
563 | throw new Exception($text . PHP_EOL . $exception->getMessage()); | |
564 | } | |
565 | ||
f5ceb6c2 | 566 | } |
a964ead0 | 567 |