Merge branch 'MDL-42965_master' of https://github.com/totara/openbadges
[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
1303eb29
DM
32use 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 */
58class 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
f5ceb6c2
DM
70 /**
71 * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
72 *
73 * Includes config.php to use moodle codebase with $CFG->behat_*
74 * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
75 *
76 * @static
77 * @throws Exception
78 * @BeforeSuite
79 */
80 public static function before_suite($event) {
81 global $CFG;
82
cfcbc34a
DM
83 // Defined only when the behat CLI command is running, the moodle init setup process will
84 // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
85 // the normal site.
86 define('BEHAT_TEST', 1);
87
f5ceb6c2
DM
88 define('CLI_SCRIPT', 1);
89
cfcbc34a 90 // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
f5ceb6c2
DM
91 require_once(__DIR__ . '/../../../config.php');
92
93 // Now that we are MOODLE_INTERNAL.
94 require_once(__DIR__ . '/../../behat/classes/behat_command.php');
17344d4c 95 require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
f5ceb6c2
DM
96 require_once(__DIR__ . '/../../behat/classes/util.php');
97 require_once(__DIR__ . '/../../testing/classes/test_lock.php');
b08b0a28 98 require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
f5ceb6c2
DM
99
100 // Avoids vendor/bin/behat to be executed directly without test environment enabled
101 // to prevent undesired db & dataroot modifications, this is also checked
102 // before each scenario (accidental user deletes) in the BeforeScenario hook.
103
104 if (!behat_util::is_test_mode_enabled()) {
105 throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
106 }
107
108 if (!behat_util::is_server_running()) {
3b7d3fb8 109 throw new Exception($CFG->behat_wwwroot .
60129d5d 110 ' is not available, ensure you specified correct url and that the server is set up and started.' .
3b7d3fb8 111 ' More info in ' . behat_command::DOCS_URL . '#Running_tests');
f5ceb6c2
DM
112 }
113
b831d479
DM
114 // Prevents using outdated data, upgrade script would start and tests would fail.
115 if (!behat_util::is_test_data_updated()) {
b32ca4ca 116 $commandpath = 'php admin/tool/behat/cli/init.php';
b831d479
DM
117 throw new Exception('Your behat test site is outdated, please run ' . $commandpath . ' from your moodle dirroot to drop and install the behat test site again.');
118 }
f5ceb6c2
DM
119 // Avoid parallel tests execution, it continues when the previous lock is released.
120 test_lock::acquire('behat');
41eb672b
DM
121
122 // Store the browser reset time if reset after N seconds is specified in config.php.
123 if (!empty($CFG->behat_restart_browser_after)) {
124 // Store the initial browser session opening.
125 self::$lastbrowsersessionstart = time();
126 }
f5ceb6c2
DM
127 }
128
129 /**
130 * Resets the test environment.
131 *
132 * @throws coding_exception If here we are not using the test database it should be because of a coding error
133 * @BeforeScenario
134 */
135 public function before_scenario($event) {
136 global $DB, $SESSION, $CFG;
137
138 // As many checks as we can.
cfcbc34a
DM
139 if (!defined('BEHAT_TEST') ||
140 !defined('BEHAT_SITE_RUNNING') ||
f5ceb6c2
DM
141 php_sapi_name() != 'cli' ||
142 !behat_util::is_test_mode_enabled() ||
cfcbc34a 143 !behat_util::is_test_site()) {
46ac40cd 144 throw new coding_exception('Behat only can modify the test database and the test dataroot!');
f5ceb6c2
DM
145 }
146
89cf999a
DM
147 $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
148 $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
f59f6b6b
DM
149 try {
150 $session = $this->getSession();
151 } catch (CurlExec $e) {
152 // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
153 // behat_util::is_server_running() we already checked that the server is running.
89cf999a
DM
154 throw new Exception($driverexceptionmsg);
155 } catch (DriverException $e) {
156 throw new Exception($driverexceptionmsg);
f59f6b6b
DM
157 } catch (UnknownError $e) {
158 // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
159 $this->throw_unknown_exception($e);
160 }
161
162
17344d4c
DM
163 // We need the Mink session to do it and we do it only before the first scenario.
164 if (self::is_first_scenario()) {
f59f6b6b 165 behat_selectors::register_moodle_selectors($session);
17344d4c
DM
166 }
167
de230fd3
PS
168 // Reset $SESSION.
169 $_SESSION = array();
c8619f33 170 $SESSION = new stdClass();
de230fd3 171 $_SESSION['SESSION'] =& $SESSION;
c8619f33 172
f5ceb6c2
DM
173 behat_util::reset_database();
174 behat_util::reset_dataroot();
175
10dd80c2
DM
176 purge_all_caches();
177 accesslib_clear_all_caches(true);
178
b08b0a28
DM
179 // Reset the nasty strings list used during the last test.
180 nasty_strings::reset_used_strings();
181
de230fd3 182 // Assign valid data to admin user (some generator-related code needs a valid user).
f5ceb6c2 183 $user = $DB->get_record('user', array('username' => 'admin'));
d79d5ac2 184 \core\session\manager::set_user($user);
5f4b4e91 185
41eb672b
DM
186 // Reset the browser if specified in config.php.
187 if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) {
188 $now = time();
189 if (self::$lastbrowsersessionstart + $CFG->behat_restart_browser_after < $now) {
f59f6b6b 190 $session->restart();
41eb672b
DM
191 self::$lastbrowsersessionstart = $now;
192 }
193 }
194
5f4b4e91 195 // Start always in the the homepage.
3b7d3fb8 196 try {
f59f6b6b
DM
197 // Let's be conservative as we never know when new upstream issues will affect us.
198 $session->visit($this->locate_path('/'));
3b7d3fb8 199 } catch (UnknownError $e) {
3b7d3fb8
DM
200 $this->throw_unknown_exception($e);
201 }
870349ee 202
f59f6b6b 203
95b43d6b
DM
204 // Checking that the root path is a Moodle test site.
205 if (self::is_first_scenario()) {
206 $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
3b7d3fb8 207 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
19f6703d 208 $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
95b43d6b
DM
209
210 self::$initprocessesfinished = true;
211 }
212
f5ceb6c2
DM
213 }
214
f5ceb6c2 215 /**
d1e55a47 216 * Wait for JS to complete before beginning interacting with the DOM.
f5ceb6c2
DM
217 *
218 * Executed only when running against a real browser.
219 *
6e2c417c 220 * @BeforeStep @javascript
f5ceb6c2 221 */
6e2c417c 222 public function before_step_javascript($event) {
d1e55a47
DM
223 $this->wait_for_pending_js();
224 }
225
226 /**
227 * Wait for JS to complete after finishing the step.
228 *
229 * With this we ensure that there are not AJAX calls
230 * still in progress.
231 *
232 * Executed only when running against a real browser.
233 *
234 * @AfterStep @javascript
235 */
236 public function after_step_javascript($event) {
237 $this->wait_for_pending_js();
238 }
239
240 /**
241 * Waits for all the JS to be loaded.
242 *
243 * @throws NoSuchWindow
244 * @throws UnknownError
245 * @return bool True or false depending whether all the JS is loaded or not.
246 */
247 protected function wait_for_pending_js() {
248
c1faf86b
DM
249 // We don't use behat_base::spin() here as we don't want to end up with an exception
250 // if the page & JSs don't finish loading properly.
251 for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
6e2c417c
DW
252 $pending = '';
253 try {
d1e55a47
DM
254 $jscode = 'return ' . self::PAGE_READY_JS . ' ? "" : M.util.pending_js.join(":");';
255 $pending = $this->getSession()->evaluateScript($jscode);
6e2c417c 256 } catch (NoSuchWindow $nsw) {
d1e55a47 257 // We catch an exception here, in case we just closed the window we were interacting with.
6e2c417c
DW
258 // No javascript is running if there is no window right?
259 $pending = '';
d1e55a47
DM
260 } catch (UnknownError $e) {
261 // Same exception as before, but some combinations of browser + OS reports it as an unknown error
262 // exception.
263 $pending = '';
28abad1a 264 }
d1e55a47
DM
265
266 // If there are no pending JS we stop waiting.
6e2c417c 267 if ($pending === '') {
d1e55a47 268 return true;
6e2c417c 269 }
d1e55a47 270
6e2c417c
DW
271 // 0.1 seconds.
272 usleep(100000);
1303eb29 273 }
d1e55a47 274
6e2c417c 275 // Timeout waiting for JS to complete.
c1faf86b 276 // TODO MDL-43173 We should fail the scenarios if JS loading times out.
d1e55a47 277 return false;
f5ceb6c2
DM
278 }
279
5f4b4e91
DM
280 /**
281 * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
282 *
283 * Part of behat_hooks class as is part of the testing framework, is auto-executed
284 * after each step so no features will splicitly use it.
285 *
286 * @Given /^I look for exceptions$/
287 * @see Moodle\BehatExtension\Tester\MoodleStepTester
288 */
289 public function i_look_for_exceptions() {
290
217e8e59
DM
291 // Wrap in try in case we were interacting with a closed window.
292 try {
5f4b4e91 293
217e8e59 294 // Exceptions.
3e76c7fa 295 $exceptionsxpath = "//div[@data-rel='fatalerror']";
8aea365f 296 // Debugging messages.
3e76c7fa 297 $debuggingxpath = "//div[@data-rel='debugging']";
8aea365f 298 // PHP debug messages.
3e76c7fa 299 $phperrorxpath = "//div[@data-rel='phpdebugmessage']";
8aea365f 300 // Any other backtrace.
6cd21356 301 $othersxpath = "(//*[contains(., ': call to ')])[1]";
8aea365f
DM
302
303 $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath);
304 $joinedxpath = implode(' | ', $xpaths);
305
306 // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check
307 // is faster than to send the 4 xpath queries for each step.
308 if (!$this->getSession()->getDriver()->find($joinedxpath)) {
309 return;
310 }
311
312 // Exceptions.
313 if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) {
5f4b4e91 314
217e8e59
DM
315 // Getting the debugging info and the backtrace.
316 $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
317 $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
318 $this->get_debug_text($errorinfoboxes[1]->getHtml());
5f4b4e91 319
217e8e59
DM
320 $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
321 throw new \Exception(html_entity_decode($msg));
322 }
323
324 // Debugging messages.
8aea365f 325 if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) {
217e8e59
DM
326 $msgs = array();
327 foreach ($debuggingmessages as $debuggingmessage) {
328 $msgs[] = $this->get_debug_text($debuggingmessage->getHtml());
329 }
330 $msg = "debugging() message/s found:\n" . implode("\n", $msgs);
331 throw new \Exception(html_entity_decode($msg));
5f4b4e91 332 }
90ed22ab 333
217e8e59 334 // PHP debug messages.
8aea365f 335 if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) {
90ed22ab 336
217e8e59
DM
337 $msgs = array();
338 foreach ($phpmessages as $phpmessage) {
339 $msgs[] = $this->get_debug_text($phpmessage->getHtml());
340 }
341 $msg = "PHP debug message/s found:\n" . implode("\n", $msgs);
342 throw new \Exception(html_entity_decode($msg));
90ed22ab 343 }
bbd802f7 344
217e8e59 345 // Any other backtrace.
9a1f4922
DM
346 // First looking through xpath as it is faster than get and parse the whole page contents,
347 // we get the contents and look for matches once we found something to suspect that there is a backtrace.
8aea365f 348 if ($this->getSession()->getDriver()->find($othersxpath)) {
9a1f4922
DM
349 $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/';
350 if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) {
351 $msgs = array();
352 foreach ($backtraces[0] as $backtrace) {
353 $msgs[] = $backtrace . '()';
354 }
355 $msg = "Other backtraces found:\n" . implode("\n", $msgs);
356 throw new \Exception(htmlentities($msg));
217e8e59 357 }
bbd802f7 358 }
217e8e59
DM
359
360 } catch (NoSuchWindow $e) {
361 // If we were interacting with a popup window it will not exists after closing it.
bbd802f7 362 }
5f4b4e91
DM
363 }
364
365 /**
366 * Converts HTML tags to line breaks to display the info in CLI
367 *
368 * @param string $html
369 * @return string
370 */
371 protected function get_debug_text($html) {
372
373 // Replacing HTML tags for new lines and keeping only the text.
374 $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html);
375 return preg_replace("/(\n)+/s", "\n", $notags);
376 }
377
95b43d6b
DM
378 /**
379 * Returns whether the first scenario of the suite is running
380 *
381 * @return bool
382 */
383 protected static function is_first_scenario() {
384 return !(self::$initprocessesfinished);
385 }
3b7d3fb8
DM
386
387 /**
388 * Throws an exception after appending an extra info text.
389 *
390 * @throws Exception
391 * @param UnknownError $exception
392 * @return void
393 */
394 protected function throw_unknown_exception(UnknownError $exception) {
395 $text = get_string('unknownexceptioninfo', 'tool_behat');
396 throw new Exception($text . PHP_EOL . $exception->getMessage());
397 }
398
f5ceb6c2 399}