weekly release 2.6dev
[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,
870349ee
DM
35 WebDriver\Exception\NoSuchWindow as NoSuchWindow,
36 WebDriver\Exception\UnexpectedAlertOpen as UnexpectedAlertOpen,
3b7d3fb8
DM
37 WebDriver\Exception\UnknownError as UnknownError,
38 WebDriver\Exception\CurlExec as CurlExec,
870349ee 39 WebDriver\Exception\NoAlertOpenError as NoAlertOpenError;
f5ceb6c2
DM
40
41/**
42 * Hooks to the behat process.
43 *
44 * Behat accepts hooks after and before each
45 * suite, feature, scenario and step.
46 *
47 * They can not call other steps as part of their process
48 * like regular steps definitions does.
49 *
50 * Throws generic Exception because they are captured by Behat.
51 *
52 * @package core
53 * @category test
54 * @copyright 2012 David Monllaó
55 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
56 */
57class behat_hooks extends behat_base {
58
41eb672b
DM
59 /**
60 * @var Last browser session start time.
61 */
62 protected static $lastbrowsersessionstart = 0;
63
95b43d6b
DM
64 /**
65 * @var For actions that should only run once.
66 */
67 protected static $initprocessesfinished = false;
68
f5ceb6c2
DM
69 /**
70 * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
71 *
72 * Includes config.php to use moodle codebase with $CFG->behat_*
73 * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
74 *
75 * @static
76 * @throws Exception
77 * @BeforeSuite
78 */
79 public static function before_suite($event) {
80 global $CFG;
81
cfcbc34a
DM
82 // Defined only when the behat CLI command is running, the moodle init setup process will
83 // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
84 // the normal site.
85 define('BEHAT_TEST', 1);
86
f5ceb6c2
DM
87 define('CLI_SCRIPT', 1);
88
cfcbc34a 89 // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
f5ceb6c2
DM
90 require_once(__DIR__ . '/../../../config.php');
91
92 // Now that we are MOODLE_INTERNAL.
93 require_once(__DIR__ . '/../../behat/classes/behat_command.php');
17344d4c 94 require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
f5ceb6c2
DM
95 require_once(__DIR__ . '/../../behat/classes/util.php');
96 require_once(__DIR__ . '/../../testing/classes/test_lock.php');
b08b0a28 97 require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
f5ceb6c2
DM
98
99 // Avoids vendor/bin/behat to be executed directly without test environment enabled
100 // to prevent undesired db & dataroot modifications, this is also checked
101 // before each scenario (accidental user deletes) in the BeforeScenario hook.
102
103 if (!behat_util::is_test_mode_enabled()) {
104 throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
105 }
106
107 if (!behat_util::is_server_running()) {
3b7d3fb8
DM
108 throw new Exception($CFG->behat_wwwroot .
109 ' is not available, ensure you started your PHP built-in server or your web server is correctly started and set up.' .
110 ' More info in ' . behat_command::DOCS_URL . '#Running_tests');
f5ceb6c2
DM
111 }
112
b831d479
DM
113 // Prevents using outdated data, upgrade script would start and tests would fail.
114 if (!behat_util::is_test_data_updated()) {
b32ca4ca 115 $commandpath = 'php admin/tool/behat/cli/init.php';
b831d479
DM
116 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.');
117 }
f5ceb6c2
DM
118 // Avoid parallel tests execution, it continues when the previous lock is released.
119 test_lock::acquire('behat');
41eb672b
DM
120
121 // Store the browser reset time if reset after N seconds is specified in config.php.
122 if (!empty($CFG->behat_restart_browser_after)) {
123 // Store the initial browser session opening.
124 self::$lastbrowsersessionstart = time();
125 }
f5ceb6c2
DM
126 }
127
128 /**
129 * Resets the test environment.
130 *
131 * @throws coding_exception If here we are not using the test database it should be because of a coding error
132 * @BeforeScenario
133 */
134 public function before_scenario($event) {
135 global $DB, $SESSION, $CFG;
136
137 // As many checks as we can.
cfcbc34a
DM
138 if (!defined('BEHAT_TEST') ||
139 !defined('BEHAT_SITE_RUNNING') ||
f5ceb6c2
DM
140 php_sapi_name() != 'cli' ||
141 !behat_util::is_test_mode_enabled() ||
cfcbc34a 142 !behat_util::is_test_site()) {
46ac40cd 143 throw new coding_exception('Behat only can modify the test database and the test dataroot!');
f5ceb6c2
DM
144 }
145
f59f6b6b
DM
146 try {
147 $session = $this->getSession();
148 } catch (CurlExec $e) {
149 // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
150 // behat_util::is_server_running() we already checked that the server is running.
151 $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
152 $msg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
153 throw new Exception($msg);
154 } catch (UnknownError $e) {
155 // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
156 $this->throw_unknown_exception($e);
157 }
158
159
17344d4c
DM
160 // We need the Mink session to do it and we do it only before the first scenario.
161 if (self::is_first_scenario()) {
f59f6b6b 162 behat_selectors::register_moodle_selectors($session);
17344d4c
DM
163 }
164
de230fd3
PS
165 // Reset $SESSION.
166 $_SESSION = array();
c8619f33 167 $SESSION = new stdClass();
de230fd3 168 $_SESSION['SESSION'] =& $SESSION;
c8619f33 169
f5ceb6c2
DM
170 behat_util::reset_database();
171 behat_util::reset_dataroot();
172
10dd80c2
DM
173 purge_all_caches();
174 accesslib_clear_all_caches(true);
175
b08b0a28
DM
176 // Reset the nasty strings list used during the last test.
177 nasty_strings::reset_used_strings();
178
de230fd3 179 // Assign valid data to admin user (some generator-related code needs a valid user).
f5ceb6c2 180 $user = $DB->get_record('user', array('username' => 'admin'));
d79d5ac2 181 \core\session\manager::set_user($user);
5f4b4e91 182
41eb672b
DM
183 // Reset the browser if specified in config.php.
184 if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) {
185 $now = time();
186 if (self::$lastbrowsersessionstart + $CFG->behat_restart_browser_after < $now) {
f59f6b6b 187 $session->restart();
41eb672b
DM
188 self::$lastbrowsersessionstart = $now;
189 }
190 }
191
5f4b4e91 192 // Start always in the the homepage.
3b7d3fb8 193 try {
f59f6b6b
DM
194 // Let's be conservative as we never know when new upstream issues will affect us.
195 $session->visit($this->locate_path('/'));
3b7d3fb8 196 } catch (UnknownError $e) {
3b7d3fb8
DM
197 $this->throw_unknown_exception($e);
198 }
870349ee 199
f59f6b6b 200
95b43d6b
DM
201 // Checking that the root path is a Moodle test site.
202 if (self::is_first_scenario()) {
203 $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
3b7d3fb8 204 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
19f6703d 205 $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
95b43d6b
DM
206
207 self::$initprocessesfinished = true;
208 }
209
870349ee
DM
210 // Closing JS dialogs if present. Otherwise they would block this scenario execution.
211 if ($this->running_javascript()) {
212 try {
f59f6b6b 213 $session->getDriver()->getWebDriverSession()->accept_alert();
870349ee
DM
214 } catch (NoAlertOpenError $e) {
215 // All ok, there should not be JS dialogs in theory.
216 }
217 }
218
f5ceb6c2
DM
219 }
220
f5ceb6c2
DM
221 /**
222 * Checks that all DOM is ready.
223 *
224 * Executed only when running against a real browser.
225 *
226 * @AfterStep @javascript
227 */
228 public function after_step_javascript($event) {
229
230 // If it doesn't have definition or it fails there is no need to check it.
231 if ($event->getResult() != StepEvent::PASSED ||
232 !$event->hasDefinition()) {
233 return;
234 }
235
28abad1a
DM
236 // Wait until the page is ready.
237 // We are already checking that we use a JS browser, this could
238 // change in case we use another JS driver.
239 try {
240
241 // Safari and Internet Explorer requires time between steps,
242 // otherwise Selenium tries to click in the previous page's DOM.
243 if ($this->getSession()->getDriver()->getBrowserName() == 'safari' ||
244 $this->getSession()->getDriver()->getBrowserName() == 'internet explorer') {
245 $this->getSession()->wait(self::TIMEOUT * 1000, false);
246
247 } else {
248 // With other browsers we just wait for the DOM ready.
249 $this->getSession()->wait(self::TIMEOUT * 1000, '(document.readyState === "complete")');
250 }
251
1303eb29
DM
252 } catch (NoSuchWindow $e) {
253 // If we were interacting with a popup window it will not exists after closing it.
3b7d3fb8
DM
254 } catch (UnknownError $e) {
255 // Custom exception to provide more feedback about possible solutions.
256 $this->throw_unknown_exception($e);
1303eb29 257 }
f5ceb6c2
DM
258 }
259
5f4b4e91
DM
260 /**
261 * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
262 *
263 * Part of behat_hooks class as is part of the testing framework, is auto-executed
264 * after each step so no features will splicitly use it.
265 *
266 * @Given /^I look for exceptions$/
267 * @see Moodle\BehatExtension\Tester\MoodleStepTester
268 */
269 public function i_look_for_exceptions() {
270
217e8e59
DM
271 // Wrap in try in case we were interacting with a closed window.
272 try {
5f4b4e91 273
217e8e59 274 // Exceptions.
3e76c7fa 275 $exceptionsxpath = "//div[@data-rel='fatalerror']";
8aea365f 276 // Debugging messages.
3e76c7fa 277 $debuggingxpath = "//div[@data-rel='debugging']";
8aea365f 278 // PHP debug messages.
3e76c7fa 279 $phperrorxpath = "//div[@data-rel='phpdebugmessage']";
8aea365f 280 // Any other backtrace.
6cd21356 281 $othersxpath = "(//*[contains(., ': call to ')])[1]";
8aea365f
DM
282
283 $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath);
284 $joinedxpath = implode(' | ', $xpaths);
285
286 // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check
287 // is faster than to send the 4 xpath queries for each step.
288 if (!$this->getSession()->getDriver()->find($joinedxpath)) {
289 return;
290 }
291
292 // Exceptions.
293 if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) {
5f4b4e91 294
217e8e59
DM
295 // Getting the debugging info and the backtrace.
296 $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
297 $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
298 $this->get_debug_text($errorinfoboxes[1]->getHtml());
5f4b4e91 299
217e8e59
DM
300 $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
301 throw new \Exception(html_entity_decode($msg));
302 }
303
304 // Debugging messages.
8aea365f 305 if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) {
217e8e59
DM
306 $msgs = array();
307 foreach ($debuggingmessages as $debuggingmessage) {
308 $msgs[] = $this->get_debug_text($debuggingmessage->getHtml());
309 }
310 $msg = "debugging() message/s found:\n" . implode("\n", $msgs);
311 throw new \Exception(html_entity_decode($msg));
5f4b4e91 312 }
90ed22ab 313
217e8e59 314 // PHP debug messages.
8aea365f 315 if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) {
90ed22ab 316
217e8e59
DM
317 $msgs = array();
318 foreach ($phpmessages as $phpmessage) {
319 $msgs[] = $this->get_debug_text($phpmessage->getHtml());
320 }
321 $msg = "PHP debug message/s found:\n" . implode("\n", $msgs);
322 throw new \Exception(html_entity_decode($msg));
90ed22ab 323 }
bbd802f7 324
217e8e59 325 // Any other backtrace.
9a1f4922
DM
326 // First looking through xpath as it is faster than get and parse the whole page contents,
327 // we get the contents and look for matches once we found something to suspect that there is a backtrace.
8aea365f 328 if ($this->getSession()->getDriver()->find($othersxpath)) {
9a1f4922
DM
329 $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/';
330 if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) {
331 $msgs = array();
332 foreach ($backtraces[0] as $backtrace) {
333 $msgs[] = $backtrace . '()';
334 }
335 $msg = "Other backtraces found:\n" . implode("\n", $msgs);
336 throw new \Exception(htmlentities($msg));
217e8e59 337 }
bbd802f7 338 }
217e8e59
DM
339
340 } catch (NoSuchWindow $e) {
341 // If we were interacting with a popup window it will not exists after closing it.
870349ee
DM
342 } catch (UnexpectedAlertOpen $e) {
343 // We fail the scenario if we find an opened JS alert/confirm, in most of the cases it
344 // will be there because we are leaving an edited form without submitting/cancelling
345 // it, but moodle is using JS confirms and we can not just cancel the JS dialog
346 // as in some cases (delete activity with JS enabled for example) the test writer should
347 // use extra steps to deal with moodle's behaviour.
348 throw new Exception('Modal window present. Ensure there are no edited forms pending to submit/cancel.');
bbd802f7 349 }
5f4b4e91
DM
350 }
351
352 /**
353 * Converts HTML tags to line breaks to display the info in CLI
354 *
355 * @param string $html
356 * @return string
357 */
358 protected function get_debug_text($html) {
359
360 // Replacing HTML tags for new lines and keeping only the text.
361 $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html);
362 return preg_replace("/(\n)+/s", "\n", $notags);
363 }
364
95b43d6b
DM
365 /**
366 * Returns whether the first scenario of the suite is running
367 *
368 * @return bool
369 */
370 protected static function is_first_scenario() {
371 return !(self::$initprocessesfinished);
372 }
3b7d3fb8
DM
373
374 /**
375 * Throws an exception after appending an extra info text.
376 *
377 * @throws Exception
378 * @param UnknownError $exception
379 * @return void
380 */
381 protected function throw_unknown_exception(UnknownError $exception) {
382 $text = get_string('unknownexceptioninfo', 'tool_behat');
383 throw new Exception($text . PHP_EOL . $exception->getMessage());
384 }
385
f5ceb6c2 386}