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