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