root / drupal7 / modules / simpletest / drupal_web_test_case.php @ 152165a8
1 | 85ad3d82 | Assos Assos | <?php
|
---|---|---|---|
2 | |||
3 | /**
|
||
4 | * Global variable that holds information about the tests being run.
|
||
5 | *
|
||
6 | * An array, with the following keys:
|
||
7 | * - 'test_run_id': the ID of the test being run, in the form 'simpletest_%"
|
||
8 | * - 'in_child_site': TRUE if the current request is a cURL request from
|
||
9 | * the parent site.
|
||
10 | *
|
||
11 | * @var array
|
||
12 | */
|
||
13 | global $drupal_test_info; |
||
14 | |||
15 | /**
|
||
16 | * Base class for Drupal tests.
|
||
17 | *
|
||
18 | * Do not extend this class, use one of the subclasses in this file.
|
||
19 | */
|
||
20 | abstract class DrupalTestCase { |
||
21 | /**
|
||
22 | * The test run ID.
|
||
23 | *
|
||
24 | * @var string
|
||
25 | */
|
||
26 | protected $testId; |
||
27 | |||
28 | /**
|
||
29 | * The database prefix of this test run.
|
||
30 | *
|
||
31 | * @var string
|
||
32 | */
|
||
33 | protected $databasePrefix = NULL; |
||
34 | |||
35 | /**
|
||
36 | * The original file directory, before it was changed for testing purposes.
|
||
37 | *
|
||
38 | * @var string
|
||
39 | */
|
||
40 | protected $originalFileDirectory = NULL; |
||
41 | |||
42 | /**
|
||
43 | * Time limit for the test.
|
||
44 | */
|
||
45 | protected $timeLimit = 500; |
||
46 | |||
47 | /**
|
||
48 | * Current results of this test case.
|
||
49 | *
|
||
50 | * @var Array
|
||
51 | */
|
||
52 | public $results = array( |
||
53 | '#pass' => 0, |
||
54 | '#fail' => 0, |
||
55 | '#exception' => 0, |
||
56 | '#debug' => 0, |
||
57 | ); |
||
58 | |||
59 | /**
|
||
60 | * Assertions thrown in that test case.
|
||
61 | *
|
||
62 | * @var Array
|
||
63 | */
|
||
64 | protected $assertions = array(); |
||
65 | |||
66 | /**
|
||
67 | * This class is skipped when looking for the source of an assertion.
|
||
68 | *
|
||
69 | * When displaying which function an assert comes from, it's not too useful
|
||
70 | * to see "drupalWebTestCase->drupalLogin()', we would like to see the test
|
||
71 | * that called it. So we need to skip the classes defining these helper
|
||
72 | * methods.
|
||
73 | */
|
||
74 | protected $skipClasses = array(__CLASS__ => TRUE); |
||
75 | |||
76 | /**
|
||
77 | * Flag to indicate whether the test has been set up.
|
||
78 | *
|
||
79 | * The setUp() method isolates the test from the parent Drupal site by
|
||
80 | * creating a random prefix for the database and setting up a clean file
|
||
81 | * storage directory. The tearDown() method then cleans up this test
|
||
82 | * environment. We must ensure that setUp() has been run. Otherwise,
|
||
83 | * tearDown() will act on the parent Drupal site rather than the test
|
||
84 | * environment, destroying live data.
|
||
85 | */
|
||
86 | protected $setup = FALSE; |
||
87 | |||
88 | protected $setupDatabasePrefix = FALSE; |
||
89 | |||
90 | protected $setupEnvironment = FALSE; |
||
91 | |||
92 | /**
|
||
93 | * Constructor for DrupalTestCase.
|
||
94 | *
|
||
95 | * @param $test_id
|
||
96 | * Tests with the same id are reported together.
|
||
97 | */
|
||
98 | public function __construct($test_id = NULL) { |
||
99 | $this->testId = $test_id; |
||
100 | } |
||
101 | |||
102 | /**
|
||
103 | * Internal helper: stores the assert.
|
||
104 | *
|
||
105 | * @param $status
|
||
106 | * Can be 'pass', 'fail', 'exception'.
|
||
107 | * TRUE is a synonym for 'pass', FALSE for 'fail'.
|
||
108 | * @param $message
|
||
109 | * The message string.
|
||
110 | * @param $group
|
||
111 | * Which group this assert belongs to.
|
||
112 | * @param $caller
|
||
113 | * By default, the assert comes from a function whose name starts with
|
||
114 | * 'test'. Instead, you can specify where this assert originates from
|
||
115 | * by passing in an associative array as $caller. Key 'file' is
|
||
116 | * the name of the source file, 'line' is the line number and 'function'
|
||
117 | * is the caller function itself.
|
||
118 | */
|
||
119 | protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) { |
||
120 | // Convert boolean status to string status.
|
||
121 | if (is_bool($status)) { |
||
122 | $status = $status ? 'pass' : 'fail'; |
||
123 | } |
||
124 | |||
125 | // Increment summary result counter.
|
||
126 | $this->results['#' . $status]++; |
||
127 | |||
128 | // Get the function information about the call to the assertion method.
|
||
129 | if (!$caller) { |
||
130 | $caller = $this->getAssertionCall(); |
||
131 | } |
||
132 | |||
133 | // Creation assertion array that can be displayed while tests are running.
|
||
134 | $this->assertions[] = $assertion = array( |
||
135 | 'test_id' => $this->testId, |
||
136 | 'test_class' => get_class($this), |
||
137 | 'status' => $status, |
||
138 | 'message' => $message, |
||
139 | 'message_group' => $group, |
||
140 | 'function' => $caller['function'], |
||
141 | 'line' => $caller['line'], |
||
142 | 'file' => $caller['file'], |
||
143 | ); |
||
144 | |||
145 | // Store assertion for display after the test has completed.
|
||
146 | try {
|
||
147 | $connection = Database::getConnection('default', 'simpletest_original_default'); |
||
148 | } |
||
149 | catch (DatabaseConnectionNotDefinedException $e) { |
||
150 | // If the test was not set up, the simpletest_original_default
|
||
151 | // connection does not exist.
|
||
152 | $connection = Database::getConnection('default', 'default'); |
||
153 | } |
||
154 | $connection
|
||
155 | ->insert('simpletest')
|
||
156 | ->fields($assertion)
|
||
157 | ->execute(); |
||
158 | |||
159 | // We do not use a ternary operator here to allow a breakpoint on
|
||
160 | // test failure.
|
||
161 | if ($status == 'pass') { |
||
162 | return TRUE; |
||
163 | } |
||
164 | else {
|
||
165 | return FALSE; |
||
166 | } |
||
167 | } |
||
168 | |||
169 | /**
|
||
170 | * Store an assertion from outside the testing context.
|
||
171 | *
|
||
172 | * This is useful for inserting assertions that can only be recorded after
|
||
173 | * the test case has been destroyed, such as PHP fatal errors. The caller
|
||
174 | * information is not automatically gathered since the caller is most likely
|
||
175 | * inserting the assertion on behalf of other code. In all other respects
|
||
176 | * the method behaves just like DrupalTestCase::assert() in terms of storing
|
||
177 | * the assertion.
|
||
178 | *
|
||
179 | * @return
|
||
180 | * Message ID of the stored assertion.
|
||
181 | *
|
||
182 | * @see DrupalTestCase::assert()
|
||
183 | * @see DrupalTestCase::deleteAssert()
|
||
184 | */
|
||
185 | public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = array()) { |
||
186 | // Convert boolean status to string status.
|
||
187 | if (is_bool($status)) { |
||
188 | $status = $status ? 'pass' : 'fail'; |
||
189 | } |
||
190 | |||
191 | $caller += array( |
||
192 | 'function' => t('Unknown'), |
||
193 | 'line' => 0, |
||
194 | 'file' => t('Unknown'), |
||
195 | ); |
||
196 | |||
197 | $assertion = array( |
||
198 | 'test_id' => $test_id, |
||
199 | 'test_class' => $test_class, |
||
200 | 'status' => $status, |
||
201 | 'message' => $message, |
||
202 | 'message_group' => $group, |
||
203 | 'function' => $caller['function'], |
||
204 | 'line' => $caller['line'], |
||
205 | 'file' => $caller['file'], |
||
206 | ); |
||
207 | |||
208 | return db_insert('simpletest') |
||
209 | ->fields($assertion)
|
||
210 | ->execute(); |
||
211 | } |
||
212 | |||
213 | /**
|
||
214 | * Delete an assertion record by message ID.
|
||
215 | *
|
||
216 | * @param $message_id
|
||
217 | * Message ID of the assertion to delete.
|
||
218 | * @return
|
||
219 | * TRUE if the assertion was deleted, FALSE otherwise.
|
||
220 | *
|
||
221 | * @see DrupalTestCase::insertAssert()
|
||
222 | */
|
||
223 | public static function deleteAssert($message_id) { |
||
224 | return (bool) db_delete('simpletest') |
||
225 | ->condition('message_id', $message_id) |
||
226 | ->execute(); |
||
227 | } |
||
228 | |||
229 | /**
|
||
230 | * Cycles through backtrace until the first non-assertion method is found.
|
||
231 | *
|
||
232 | * @return
|
||
233 | * Array representing the true caller.
|
||
234 | */
|
||
235 | protected function getAssertionCall() { |
||
236 | $backtrace = debug_backtrace(); |
||
237 | |||
238 | // The first element is the call. The second element is the caller.
|
||
239 | // We skip calls that occurred in one of the methods of our base classes
|
||
240 | // or in an assertion function.
|
||
241 | while (($caller = $backtrace[1]) && |
||
242 | ((isset($caller['class']) && isset($this->skipClasses[$caller['class']])) || |
||
243 | substr($caller['function'], 0, 6) == 'assert')) { |
||
244 | // We remove that call.
|
||
245 | array_shift($backtrace); |
||
246 | } |
||
247 | |||
248 | return _drupal_get_last_caller($backtrace); |
||
249 | } |
||
250 | |||
251 | /**
|
||
252 | * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE).
|
||
253 | *
|
||
254 | * @param $value
|
||
255 | * The value on which the assertion is to be done.
|
||
256 | * @param $message
|
||
257 | * The message to display along with the assertion.
|
||
258 | * @param $group
|
||
259 | * The type of assertion - examples are "Browser", "PHP".
|
||
260 | * @return
|
||
261 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
262 | */
|
||
263 | protected function assertTrue($value, $message = '', $group = 'Other') { |
||
264 | return $this->assert((bool) $value, $message ? $message : t('Value @value is TRUE.', array('@value' => var_export($value, TRUE))), $group); |
||
265 | } |
||
266 | |||
267 | /**
|
||
268 | * Check to see if a value is false (an empty string, 0, NULL, or FALSE).
|
||
269 | *
|
||
270 | * @param $value
|
||
271 | * The value on which the assertion is to be done.
|
||
272 | * @param $message
|
||
273 | * The message to display along with the assertion.
|
||
274 | * @param $group
|
||
275 | * The type of assertion - examples are "Browser", "PHP".
|
||
276 | * @return
|
||
277 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
278 | */
|
||
279 | protected function assertFalse($value, $message = '', $group = 'Other') { |
||
280 | return $this->assert(!$value, $message ? $message : t('Value @value is FALSE.', array('@value' => var_export($value, TRUE))), $group); |
||
281 | } |
||
282 | |||
283 | /**
|
||
284 | * Check to see if a value is NULL.
|
||
285 | *
|
||
286 | * @param $value
|
||
287 | * The value on which the assertion is to be done.
|
||
288 | * @param $message
|
||
289 | * The message to display along with the assertion.
|
||
290 | * @param $group
|
||
291 | * The type of assertion - examples are "Browser", "PHP".
|
||
292 | * @return
|
||
293 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
294 | */
|
||
295 | protected function assertNull($value, $message = '', $group = 'Other') { |
||
296 | return $this->assert(!isset($value), $message ? $message : t('Value @value is NULL.', array('@value' => var_export($value, TRUE))), $group); |
||
297 | } |
||
298 | |||
299 | /**
|
||
300 | * Check to see if a value is not NULL.
|
||
301 | *
|
||
302 | * @param $value
|
||
303 | * The value on which the assertion is to be done.
|
||
304 | * @param $message
|
||
305 | * The message to display along with the assertion.
|
||
306 | * @param $group
|
||
307 | * The type of assertion - examples are "Browser", "PHP".
|
||
308 | * @return
|
||
309 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
310 | */
|
||
311 | protected function assertNotNull($value, $message = '', $group = 'Other') { |
||
312 | return $this->assert(isset($value), $message ? $message : t('Value @value is not NULL.', array('@value' => var_export($value, TRUE))), $group); |
||
313 | } |
||
314 | |||
315 | /**
|
||
316 | * Check to see if two values are equal.
|
||
317 | *
|
||
318 | * @param $first
|
||
319 | * The first value to check.
|
||
320 | * @param $second
|
||
321 | * The second value to check.
|
||
322 | * @param $message
|
||
323 | * The message to display along with the assertion.
|
||
324 | * @param $group
|
||
325 | * The type of assertion - examples are "Browser", "PHP".
|
||
326 | * @return
|
||
327 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
328 | */
|
||
329 | protected function assertEqual($first, $second, $message = '', $group = 'Other') { |
||
330 | return $this->assert($first == $second, $message ? $message : t('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); |
||
331 | } |
||
332 | |||
333 | /**
|
||
334 | * Check to see if two values are not equal.
|
||
335 | *
|
||
336 | * @param $first
|
||
337 | * The first value to check.
|
||
338 | * @param $second
|
||
339 | * The second value to check.
|
||
340 | * @param $message
|
||
341 | * The message to display along with the assertion.
|
||
342 | * @param $group
|
||
343 | * The type of assertion - examples are "Browser", "PHP".
|
||
344 | * @return
|
||
345 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
346 | */
|
||
347 | protected function assertNotEqual($first, $second, $message = '', $group = 'Other') { |
||
348 | return $this->assert($first != $second, $message ? $message : t('Value @first is not equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); |
||
349 | } |
||
350 | |||
351 | /**
|
||
352 | * Check to see if two values are identical.
|
||
353 | *
|
||
354 | * @param $first
|
||
355 | * The first value to check.
|
||
356 | * @param $second
|
||
357 | * The second value to check.
|
||
358 | * @param $message
|
||
359 | * The message to display along with the assertion.
|
||
360 | * @param $group
|
||
361 | * The type of assertion - examples are "Browser", "PHP".
|
||
362 | * @return
|
||
363 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
364 | */
|
||
365 | protected function assertIdentical($first, $second, $message = '', $group = 'Other') { |
||
366 | return $this->assert($first === $second, $message ? $message : t('Value @first is identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); |
||
367 | } |
||
368 | |||
369 | /**
|
||
370 | * Check to see if two values are not identical.
|
||
371 | *
|
||
372 | * @param $first
|
||
373 | * The first value to check.
|
||
374 | * @param $second
|
||
375 | * The second value to check.
|
||
376 | * @param $message
|
||
377 | * The message to display along with the assertion.
|
||
378 | * @param $group
|
||
379 | * The type of assertion - examples are "Browser", "PHP".
|
||
380 | * @return
|
||
381 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
382 | */
|
||
383 | protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') { |
||
384 | return $this->assert($first !== $second, $message ? $message : t('Value @first is not identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); |
||
385 | } |
||
386 | |||
387 | /**
|
||
388 | * Fire an assertion that is always positive.
|
||
389 | *
|
||
390 | * @param $message
|
||
391 | * The message to display along with the assertion.
|
||
392 | * @param $group
|
||
393 | * The type of assertion - examples are "Browser", "PHP".
|
||
394 | * @return
|
||
395 | * TRUE.
|
||
396 | */
|
||
397 | protected function pass($message = NULL, $group = 'Other') { |
||
398 | return $this->assert(TRUE, $message, $group); |
||
399 | } |
||
400 | |||
401 | /**
|
||
402 | * Fire an assertion that is always negative.
|
||
403 | *
|
||
404 | * @param $message
|
||
405 | * The message to display along with the assertion.
|
||
406 | * @param $group
|
||
407 | * The type of assertion - examples are "Browser", "PHP".
|
||
408 | * @return
|
||
409 | * FALSE.
|
||
410 | */
|
||
411 | protected function fail($message = NULL, $group = 'Other') { |
||
412 | return $this->assert(FALSE, $message, $group); |
||
413 | } |
||
414 | |||
415 | /**
|
||
416 | * Fire an error assertion.
|
||
417 | *
|
||
418 | * @param $message
|
||
419 | * The message to display along with the assertion.
|
||
420 | * @param $group
|
||
421 | * The type of assertion - examples are "Browser", "PHP".
|
||
422 | * @param $caller
|
||
423 | * The caller of the error.
|
||
424 | * @return
|
||
425 | * FALSE.
|
||
426 | */
|
||
427 | protected function error($message = '', $group = 'Other', array $caller = NULL) { |
||
428 | if ($group == 'User notice') { |
||
429 | // Since 'User notice' is set by trigger_error() which is used for debug
|
||
430 | // set the message to a status of 'debug'.
|
||
431 | return $this->assert('debug', $message, 'Debug', $caller); |
||
432 | } |
||
433 | |||
434 | return $this->assert('exception', $message, $group, $caller); |
||
435 | } |
||
436 | |||
437 | /**
|
||
438 | * Logs verbose message in a text file.
|
||
439 | *
|
||
440 | * The a link to the vebose message will be placed in the test results via
|
||
441 | * as a passing assertion with the text '[verbose message]'.
|
||
442 | *
|
||
443 | * @param $message
|
||
444 | * The verbose message to be stored.
|
||
445 | *
|
||
446 | * @see simpletest_verbose()
|
||
447 | */
|
||
448 | protected function verbose($message) { |
||
449 | if ($id = simpletest_verbose($message)) { |
||
450 | $class_safe = str_replace('\\', '_', get_class($this)); |
||
451 | $url = file_create_url($this->originalFileDirectory . '/simpletest/verbose/' . $class_safe . '-' . $id . '.html'); |
||
452 | $this->error(l(t('Verbose message'), $url, array('attributes' => array('target' => '_blank'))), 'User notice'); |
||
453 | } |
||
454 | } |
||
455 | |||
456 | /**
|
||
457 | * Run all tests in this class.
|
||
458 | *
|
||
459 | * Regardless of whether $methods are passed or not, only method names
|
||
460 | * starting with "test" are executed.
|
||
461 | *
|
||
462 | * @param $methods
|
||
463 | * (optional) A list of method names in the test case class to run; e.g.,
|
||
464 | * array('testFoo', 'testBar'). By default, all methods of the class are
|
||
465 | * taken into account, but it can be useful to only run a few selected test
|
||
466 | * methods during debugging.
|
||
467 | */
|
||
468 | public function run(array $methods = array()) { |
||
469 | // Initialize verbose debugging.
|
||
470 | $class = get_class($this); |
||
471 | simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), str_replace('\\', '_', $class)); |
||
472 | |||
473 | // HTTP auth settings (<username>:<password>) for the simpletest browser
|
||
474 | // when sending requests to the test site.
|
||
475 | $this->httpauth_method = variable_get('simpletest_httpauth_method', CURLAUTH_BASIC); |
||
476 | $username = variable_get('simpletest_httpauth_username', NULL); |
||
477 | $password = variable_get('simpletest_httpauth_password', NULL); |
||
478 | if ($username && $password) { |
||
479 | $this->httpauth_credentials = $username . ':' . $password; |
||
480 | } |
||
481 | |||
482 | set_error_handler(array($this, 'errorHandler')); |
||
483 | // Iterate through all the methods in this class, unless a specific list of
|
||
484 | // methods to run was passed.
|
||
485 | $class_methods = get_class_methods($class); |
||
486 | if ($methods) { |
||
487 | $class_methods = array_intersect($class_methods, $methods); |
||
488 | } |
||
489 | foreach ($class_methods as $method) { |
||
490 | // If the current method starts with "test", run it - it's a test.
|
||
491 | if (strtolower(substr($method, 0, 4)) == 'test') { |
||
492 | // Insert a fail record. This will be deleted on completion to ensure
|
||
493 | // that testing completed.
|
||
494 | $method_info = new ReflectionMethod($class, $method); |
||
495 | $caller = array( |
||
496 | 'file' => $method_info->getFileName(), |
||
497 | 'line' => $method_info->getStartLine(), |
||
498 | 'function' => $class . '->' . $method . '()', |
||
499 | ); |
||
500 | $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller); |
||
501 | $this->setUp();
|
||
502 | if ($this->setup) { |
||
503 | try {
|
||
504 | $this->$method(); |
||
505 | // Finish up.
|
||
506 | } |
||
507 | catch (Exception $e) { |
||
508 | $this->exceptionHandler($e); |
||
509 | } |
||
510 | $this->tearDown();
|
||
511 | } |
||
512 | else {
|
||
513 | $this->fail(t("The test cannot be executed because it has not been set up properly.")); |
||
514 | } |
||
515 | // Remove the completion check record.
|
||
516 | DrupalTestCase::deleteAssert($completion_check_id); |
||
517 | } |
||
518 | } |
||
519 | // Clear out the error messages and restore error handler.
|
||
520 | drupal_get_messages(); |
||
521 | restore_error_handler(); |
||
522 | } |
||
523 | |||
524 | /**
|
||
525 | * Handle errors during test runs.
|
||
526 | *
|
||
527 | * Because this is registered in set_error_handler(), it has to be public.
|
||
528 | * @see set_error_handler
|
||
529 | */
|
||
530 | public function errorHandler($severity, $message, $file = NULL, $line = NULL) { |
||
531 | if ($severity & error_reporting()) { |
||
532 | $error_map = array( |
||
533 | E_STRICT => 'Run-time notice', |
||
534 | E_WARNING => 'Warning', |
||
535 | E_NOTICE => 'Notice', |
||
536 | E_CORE_ERROR => 'Core error', |
||
537 | E_CORE_WARNING => 'Core warning', |
||
538 | E_USER_ERROR => 'User error', |
||
539 | E_USER_WARNING => 'User warning', |
||
540 | E_USER_NOTICE => 'User notice', |
||
541 | E_RECOVERABLE_ERROR => 'Recoverable error', |
||
542 | ); |
||
543 | |||
544 | // PHP 5.3 adds new error logging constants. Add these conditionally for
|
||
545 | // backwards compatibility with PHP 5.2.
|
||
546 | if (defined('E_DEPRECATED')) { |
||
547 | $error_map += array( |
||
548 | E_DEPRECATED => 'Deprecated', |
||
549 | E_USER_DEPRECATED => 'User deprecated', |
||
550 | ); |
||
551 | } |
||
552 | |||
553 | $backtrace = debug_backtrace(); |
||
554 | $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); |
||
555 | } |
||
556 | return TRUE; |
||
557 | } |
||
558 | |||
559 | /**
|
||
560 | * Handle exceptions.
|
||
561 | *
|
||
562 | * @see set_exception_handler
|
||
563 | */
|
||
564 | protected function exceptionHandler($exception) { |
||
565 | $backtrace = $exception->getTrace(); |
||
566 | // Push on top of the backtrace the call that generated the exception.
|
||
567 | array_unshift($backtrace, array( |
||
568 | 'line' => $exception->getLine(), |
||
569 | 'file' => $exception->getFile(), |
||
570 | )); |
||
571 | require_once DRUPAL_ROOT . '/includes/errors.inc'; |
||
572 | // The exception message is run through check_plain() by _drupal_decode_exception().
|
||
573 | $this->error(t('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)), 'Uncaught exception', _drupal_get_last_caller($backtrace)); |
||
574 | } |
||
575 | |||
576 | /**
|
||
577 | * Generates a random string of ASCII characters of codes 32 to 126.
|
||
578 | *
|
||
579 | * The generated string includes alpha-numeric characters and common
|
||
580 | * miscellaneous characters. Use this method when testing general input
|
||
581 | * where the content is not restricted.
|
||
582 | *
|
||
583 | * Do not use this method when special characters are not possible (e.g., in
|
||
584 | * machine or file names that have already been validated); instead,
|
||
585 | * use DrupalWebTestCase::randomName().
|
||
586 | *
|
||
587 | * @param $length
|
||
588 | * Length of random string to generate.
|
||
589 | *
|
||
590 | * @return
|
||
591 | * Randomly generated string.
|
||
592 | *
|
||
593 | * @see DrupalWebTestCase::randomName()
|
||
594 | */
|
||
595 | public static function randomString($length = 8) { |
||
596 | $str = ''; |
||
597 | for ($i = 0; $i < $length; $i++) { |
||
598 | $str .= chr(mt_rand(32, 126)); |
||
599 | } |
||
600 | return $str; |
||
601 | } |
||
602 | |||
603 | /**
|
||
604 | * Generates a random string containing letters and numbers.
|
||
605 | *
|
||
606 | * The string will always start with a letter. The letters may be upper or
|
||
607 | * lower case. This method is better for restricted inputs that do not
|
||
608 | * accept certain characters. For example, when testing input fields that
|
||
609 | * require machine readable values (i.e. without spaces and non-standard
|
||
610 | * characters) this method is best.
|
||
611 | *
|
||
612 | * Do not use this method when testing unvalidated user input. Instead, use
|
||
613 | * DrupalWebTestCase::randomString().
|
||
614 | *
|
||
615 | * @param $length
|
||
616 | * Length of random string to generate.
|
||
617 | *
|
||
618 | * @return
|
||
619 | * Randomly generated string.
|
||
620 | *
|
||
621 | * @see DrupalWebTestCase::randomString()
|
||
622 | */
|
||
623 | public static function randomName($length = 8) { |
||
624 | $values = array_merge(range(65, 90), range(97, 122), range(48, 57)); |
||
625 | $max = count($values) - 1; |
||
626 | $str = chr(mt_rand(97, 122)); |
||
627 | for ($i = 1; $i < $length; $i++) { |
||
628 | $str .= chr($values[mt_rand(0, $max)]); |
||
629 | } |
||
630 | return $str; |
||
631 | } |
||
632 | |||
633 | /**
|
||
634 | * Converts a list of possible parameters into a stack of permutations.
|
||
635 | *
|
||
636 | * Takes a list of parameters containing possible values, and converts all of
|
||
637 | * them into a list of items containing every possible permutation.
|
||
638 | *
|
||
639 | * Example:
|
||
640 | * @code
|
||
641 | * $parameters = array(
|
||
642 | * 'one' => array(0, 1),
|
||
643 | * 'two' => array(2, 3),
|
||
644 | * );
|
||
645 | * $permutations = DrupalTestCase::generatePermutations($parameters)
|
||
646 | * // Result:
|
||
647 | * $permutations == array(
|
||
648 | * array('one' => 0, 'two' => 2),
|
||
649 | * array('one' => 1, 'two' => 2),
|
||
650 | * array('one' => 0, 'two' => 3),
|
||
651 | * array('one' => 1, 'two' => 3),
|
||
652 | * )
|
||
653 | * @endcode
|
||
654 | *
|
||
655 | * @param $parameters
|
||
656 | * An associative array of parameters, keyed by parameter name, and whose
|
||
657 | * values are arrays of parameter values.
|
||
658 | *
|
||
659 | * @return
|
||
660 | * A list of permutations, which is an array of arrays. Each inner array
|
||
661 | * contains the full list of parameters that have been passed, but with a
|
||
662 | * single value only.
|
||
663 | */
|
||
664 | public static function generatePermutations($parameters) { |
||
665 | $all_permutations = array(array()); |
||
666 | foreach ($parameters as $parameter => $values) { |
||
667 | $new_permutations = array(); |
||
668 | // Iterate over all values of the parameter.
|
||
669 | foreach ($values as $value) { |
||
670 | // Iterate over all existing permutations.
|
||
671 | foreach ($all_permutations as $permutation) { |
||
672 | // Add the new parameter value to existing permutations.
|
||
673 | $new_permutations[] = $permutation + array($parameter => $value); |
||
674 | } |
||
675 | } |
||
676 | // Replace the old permutations with the new permutations.
|
||
677 | $all_permutations = $new_permutations; |
||
678 | } |
||
679 | return $all_permutations; |
||
680 | } |
||
681 | } |
||
682 | |||
683 | /**
|
||
684 | * Test case for Drupal unit tests.
|
||
685 | *
|
||
686 | * These tests can not access the database nor files. Calling any Drupal
|
||
687 | * function that needs the database will throw exceptions. These include
|
||
688 | * watchdog(), module_implements(), module_invoke_all() etc.
|
||
689 | */
|
||
690 | class DrupalUnitTestCase extends DrupalTestCase { |
||
691 | |||
692 | /**
|
||
693 | * Constructor for DrupalUnitTestCase.
|
||
694 | */
|
||
695 | function __construct($test_id = NULL) { |
||
696 | parent::__construct($test_id); |
||
697 | $this->skipClasses[__CLASS__] = TRUE; |
||
698 | } |
||
699 | |||
700 | /**
|
||
701 | * Sets up unit test environment.
|
||
702 | *
|
||
703 | * Unlike DrupalWebTestCase::setUp(), DrupalUnitTestCase::setUp() does not
|
||
704 | * install modules because tests are performed without accessing the database.
|
||
705 | * Any required files must be explicitly included by the child class setUp()
|
||
706 | * method.
|
||
707 | */
|
||
708 | protected function setUp() { |
||
709 | global $conf; |
||
710 | |||
711 | // Store necessary current values before switching to the test environment.
|
||
712 | $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); |
||
713 | |||
714 | // Reset all statics so that test is performed with a clean environment.
|
||
715 | drupal_static_reset(); |
||
716 | |||
717 | // Generate temporary prefixed database to ensure that tests have a clean starting point.
|
||
718 | $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); |
||
719 | |||
720 | // Create test directory.
|
||
721 | $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); |
||
722 | file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); |
||
723 | $conf['file_public_path'] = $public_files_directory; |
||
724 | |||
725 | // Clone the current connection and replace the current prefix.
|
||
726 | $connection_info = Database::getConnectionInfo('default'); |
||
727 | Database::renameConnection('default', 'simpletest_original_default'); |
||
728 | foreach ($connection_info as $target => $value) { |
||
729 | $connection_info[$target]['prefix'] = array( |
||
730 | 'default' => $value['prefix']['default'] . $this->databasePrefix, |
||
731 | ); |
||
732 | } |
||
733 | Database::addConnectionInfo('default', 'default', $connection_info['default']); |
||
734 | |||
735 | // Set user agent to be consistent with web test case.
|
||
736 | $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix; |
||
737 | |||
738 | // If locale is enabled then t() will try to access the database and
|
||
739 | // subsequently will fail as the database is not accessible.
|
||
740 | $module_list = module_list();
|
||
741 | if (isset($module_list['locale'])) { |
||
742 | // Transform the list into the format expected as input to module_list().
|
||
743 | foreach ($module_list as &$module) { |
||
744 | $module = array('filename' => drupal_get_filename('module', $module)); |
||
745 | } |
||
746 | $this->originalModuleList = $module_list; |
||
747 | unset($module_list['locale']); |
||
748 | module_list(TRUE, FALSE, FALSE, $module_list); |
||
749 | } |
||
750 | $this->setup = TRUE; |
||
751 | } |
||
752 | |||
753 | protected function tearDown() { |
||
754 | global $conf; |
||
755 | |||
756 | // Get back to the original connection.
|
||
757 | Database::removeConnection('default'); |
||
758 | Database::renameConnection('simpletest_original_default', 'default'); |
||
759 | |||
760 | $conf['file_public_path'] = $this->originalFileDirectory; |
||
761 | // Restore modules if necessary.
|
||
762 | if (isset($this->originalModuleList)) { |
||
763 | module_list(TRUE, FALSE, FALSE, $this->originalModuleList); |
||
764 | } |
||
765 | } |
||
766 | } |
||
767 | |||
768 | /**
|
||
769 | * Test case for typical Drupal tests.
|
||
770 | */
|
||
771 | class DrupalWebTestCase extends DrupalTestCase { |
||
772 | /**
|
||
773 | * The profile to install as a basis for testing.
|
||
774 | *
|
||
775 | * @var string
|
||
776 | */
|
||
777 | protected $profile = 'standard'; |
||
778 | |||
779 | /**
|
||
780 | * The URL currently loaded in the internal browser.
|
||
781 | *
|
||
782 | * @var string
|
||
783 | */
|
||
784 | protected $url; |
||
785 | |||
786 | /**
|
||
787 | * The handle of the current cURL connection.
|
||
788 | *
|
||
789 | * @var resource
|
||
790 | */
|
||
791 | protected $curlHandle; |
||
792 | |||
793 | /**
|
||
794 | * The headers of the page currently loaded in the internal browser.
|
||
795 | *
|
||
796 | * @var Array
|
||
797 | */
|
||
798 | protected $headers; |
||
799 | |||
800 | /**
|
||
801 | * The content of the page currently loaded in the internal browser.
|
||
802 | *
|
||
803 | * @var string
|
||
804 | */
|
||
805 | protected $content; |
||
806 | |||
807 | /**
|
||
808 | * The content of the page currently loaded in the internal browser (plain text version).
|
||
809 | *
|
||
810 | * @var string
|
||
811 | */
|
||
812 | protected $plainTextContent; |
||
813 | |||
814 | /**
|
||
815 | * The value of the Drupal.settings JavaScript variable for the page currently loaded in the internal browser.
|
||
816 | *
|
||
817 | * @var Array
|
||
818 | */
|
||
819 | protected $drupalSettings; |
||
820 | |||
821 | /**
|
||
822 | * The parsed version of the page.
|
||
823 | *
|
||
824 | * @var SimpleXMLElement
|
||
825 | */
|
||
826 | protected $elements = NULL; |
||
827 | |||
828 | /**
|
||
829 | * The current user logged in using the internal browser.
|
||
830 | *
|
||
831 | * @var bool
|
||
832 | */
|
||
833 | protected $loggedInUser = FALSE; |
||
834 | |||
835 | /**
|
||
836 | * The current cookie file used by cURL.
|
||
837 | *
|
||
838 | * We do not reuse the cookies in further runs, so we do not need a file
|
||
839 | * but we still need cookie handling, so we set the jar to NULL.
|
||
840 | */
|
||
841 | protected $cookieFile = NULL; |
||
842 | |||
843 | /**
|
||
844 | * Additional cURL options.
|
||
845 | *
|
||
846 | * DrupalWebTestCase itself never sets this but always obeys what is set.
|
||
847 | */
|
||
848 | protected $additionalCurlOptions = array(); |
||
849 | |||
850 | /**
|
||
851 | * The original user, before it was changed to a clean uid = 1 for testing purposes.
|
||
852 | *
|
||
853 | * @var object
|
||
854 | */
|
||
855 | protected $originalUser = NULL; |
||
856 | |||
857 | /**
|
||
858 | * The original shutdown handlers array, before it was cleaned for testing purposes.
|
||
859 | *
|
||
860 | * @var array
|
||
861 | */
|
||
862 | protected $originalShutdownCallbacks = array(); |
||
863 | |||
864 | /**
|
||
865 | * HTTP authentication method
|
||
866 | */
|
||
867 | protected $httpauth_method = CURLAUTH_BASIC; |
||
868 | |||
869 | /**
|
||
870 | * HTTP authentication credentials (<username>:<password>).
|
||
871 | */
|
||
872 | protected $httpauth_credentials = NULL; |
||
873 | |||
874 | /**
|
||
875 | * The current session name, if available.
|
||
876 | */
|
||
877 | protected $session_name = NULL; |
||
878 | |||
879 | /**
|
||
880 | * The current session ID, if available.
|
||
881 | */
|
||
882 | protected $session_id = NULL; |
||
883 | |||
884 | /**
|
||
885 | * Whether the files were copied to the test files directory.
|
||
886 | */
|
||
887 | protected $generatedTestFiles = FALSE; |
||
888 | |||
889 | /**
|
||
890 | * The number of redirects followed during the handling of a request.
|
||
891 | */
|
||
892 | protected $redirect_count; |
||
893 | |||
894 | /**
|
||
895 | * Constructor for DrupalWebTestCase.
|
||
896 | */
|
||
897 | function __construct($test_id = NULL) { |
||
898 | parent::__construct($test_id); |
||
899 | $this->skipClasses[__CLASS__] = TRUE; |
||
900 | } |
||
901 | |||
902 | /**
|
||
903 | * Get a node from the database based on its title.
|
||
904 | *
|
||
905 | * @param $title
|
||
906 | * A node title, usually generated by $this->randomName().
|
||
907 | * @param $reset
|
||
908 | * (optional) Whether to reset the internal node_load() cache.
|
||
909 | *
|
||
910 | * @return
|
||
911 | * A node object matching $title.
|
||
912 | */
|
||
913 | function drupalGetNodeByTitle($title, $reset = FALSE) { |
||
914 | $nodes = node_load_multiple(array(), array('title' => $title), $reset); |
||
915 | // Load the first node returned from the database.
|
||
916 | $returned_node = reset($nodes); |
||
917 | return $returned_node; |
||
918 | } |
||
919 | |||
920 | /**
|
||
921 | * Creates a node based on default settings.
|
||
922 | *
|
||
923 | * @param $settings
|
||
924 | * An associative array of settings to change from the defaults, keys are
|
||
925 | * node properties, for example 'title' => 'Hello, world!'.
|
||
926 | * @return
|
||
927 | * Created node object.
|
||
928 | */
|
||
929 | protected function drupalCreateNode($settings = array()) { |
||
930 | // Populate defaults array.
|
||
931 | $settings += array( |
||
932 | 'body' => array(LANGUAGE_NONE => array(array())), |
||
933 | 'title' => $this->randomName(8), |
||
934 | 'comment' => 2, |
||
935 | 'changed' => REQUEST_TIME, |
||
936 | 'moderate' => 0, |
||
937 | 'promote' => 0, |
||
938 | 'revision' => 1, |
||
939 | 'log' => '', |
||
940 | 'status' => 1, |
||
941 | 'sticky' => 0, |
||
942 | 'type' => 'page', |
||
943 | 'revisions' => NULL, |
||
944 | 'language' => LANGUAGE_NONE, |
||
945 | ); |
||
946 | |||
947 | // Use the original node's created time for existing nodes.
|
||
948 | if (isset($settings['created']) && !isset($settings['date'])) { |
||
949 | $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O'); |
||
950 | } |
||
951 | |||
952 | // If the node's user uid is not specified manually, use the currently
|
||
953 | // logged in user if available, or else the user running the test.
|
||
954 | if (!isset($settings['uid'])) { |
||
955 | if ($this->loggedInUser) { |
||
956 | $settings['uid'] = $this->loggedInUser->uid; |
||
957 | } |
||
958 | else {
|
||
959 | global $user; |
||
960 | $settings['uid'] = $user->uid; |
||
961 | } |
||
962 | } |
||
963 | |||
964 | // Merge body field value and format separately.
|
||
965 | $body = array( |
||
966 | 'value' => $this->randomName(32), |
||
967 | 'format' => filter_default_format(),
|
||
968 | ); |
||
969 | $settings['body'][$settings['language']][0] += $body; |
||
970 | |||
971 | $node = (object) $settings; |
||
972 | node_save($node);
|
||
973 | |||
974 | // Small hack to link revisions to our test user.
|
||
975 | db_update('node_revision')
|
||
976 | ->fields(array('uid' => $node->uid)) |
||
977 | ->condition('vid', $node->vid) |
||
978 | ->execute(); |
||
979 | return $node; |
||
980 | } |
||
981 | |||
982 | /**
|
||
983 | * Creates a custom content type based on default settings.
|
||
984 | *
|
||
985 | * @param $settings
|
||
986 | * An array of settings to change from the defaults.
|
||
987 | * Example: 'type' => 'foo'.
|
||
988 | * @return
|
||
989 | * Created content type.
|
||
990 | */
|
||
991 | protected function drupalCreateContentType($settings = array()) { |
||
992 | // Find a non-existent random type name.
|
||
993 | do {
|
||
994 | $name = strtolower($this->randomName(8)); |
||
995 | } while (node_type_get_type($name)); |
||
996 | |||
997 | // Populate defaults array.
|
||
998 | $defaults = array( |
||
999 | 'type' => $name, |
||
1000 | 'name' => $name, |
||
1001 | 'base' => 'node_content', |
||
1002 | 'description' => '', |
||
1003 | 'help' => '', |
||
1004 | 'title_label' => 'Title', |
||
1005 | 'body_label' => 'Body', |
||
1006 | 'has_title' => 1, |
||
1007 | 'has_body' => 1, |
||
1008 | ); |
||
1009 | // Imposed values for a custom type.
|
||
1010 | $forced = array( |
||
1011 | 'orig_type' => '', |
||
1012 | 'old_type' => '', |
||
1013 | 'module' => 'node', |
||
1014 | 'custom' => 1, |
||
1015 | 'modified' => 1, |
||
1016 | 'locked' => 0, |
||
1017 | ); |
||
1018 | $type = $forced + $settings + $defaults; |
||
1019 | $type = (object) $type; |
||
1020 | |||
1021 | $saved_type = node_type_save($type); |
||
1022 | node_types_rebuild(); |
||
1023 | menu_rebuild(); |
||
1024 | node_add_body_field($type);
|
||
1025 | |||
1026 | $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type))); |
||
1027 | |||
1028 | // Reset permissions so that permissions for this content type are available.
|
||
1029 | $this->checkPermissions(array(), TRUE); |
||
1030 | |||
1031 | return $type; |
||
1032 | } |
||
1033 | |||
1034 | /**
|
||
1035 | * Get a list files that can be used in tests.
|
||
1036 | *
|
||
1037 | * @param $type
|
||
1038 | * File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'.
|
||
1039 | * @param $size
|
||
1040 | * File size in bytes to match. Please check the tests/files folder.
|
||
1041 | * @return
|
||
1042 | * List of files that match filter.
|
||
1043 | */
|
||
1044 | protected function drupalGetTestFiles($type, $size = NULL) { |
||
1045 | if (empty($this->generatedTestFiles)) { |
||
1046 | // Generate binary test files.
|
||
1047 | $lines = array(64, 1024); |
||
1048 | $count = 0; |
||
1049 | foreach ($lines as $line) { |
||
1050 | simpletest_generate_file('binary-' . $count++, 64, $line, 'binary'); |
||
1051 | } |
||
1052 | |||
1053 | // Generate text test files.
|
||
1054 | $lines = array(16, 256, 1024, 2048, 20480); |
||
1055 | $count = 0; |
||
1056 | foreach ($lines as $line) { |
||
1057 | simpletest_generate_file('text-' . $count++, 64, $line); |
||
1058 | } |
||
1059 | |||
1060 | // Copy other test files from simpletest.
|
||
1061 | $original = drupal_get_path('module', 'simpletest') . '/files'; |
||
1062 | $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/'); |
||
1063 | foreach ($files as $file) { |
||
1064 | file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files')); |
||
1065 | } |
||
1066 | |||
1067 | $this->generatedTestFiles = TRUE; |
||
1068 | } |
||
1069 | |||
1070 | $files = array(); |
||
1071 | // Make sure type is valid.
|
||
1072 | if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) { |
||
1073 | $files = file_scan_directory('public://', '/' . $type . '\-.*/'); |
||
1074 | |||
1075 | // If size is set then remove any files that are not of that size.
|
||
1076 | if ($size !== NULL) { |
||
1077 | foreach ($files as $file) { |
||
1078 | $stats = stat($file->uri); |
||
1079 | if ($stats['size'] != $size) { |
||
1080 | unset($files[$file->uri]); |
||
1081 | } |
||
1082 | } |
||
1083 | } |
||
1084 | } |
||
1085 | usort($files, array($this, 'drupalCompareFiles')); |
||
1086 | return $files; |
||
1087 | } |
||
1088 | |||
1089 | /**
|
||
1090 | * Compare two files based on size and file name.
|
||
1091 | */
|
||
1092 | protected function drupalCompareFiles($file1, $file2) { |
||
1093 | $compare_size = filesize($file1->uri) - filesize($file2->uri); |
||
1094 | if ($compare_size) { |
||
1095 | // Sort by file size.
|
||
1096 | return $compare_size; |
||
1097 | } |
||
1098 | else {
|
||
1099 | // The files were the same size, so sort alphabetically.
|
||
1100 | return strnatcmp($file1->name, $file2->name); |
||
1101 | } |
||
1102 | } |
||
1103 | |||
1104 | /**
|
||
1105 | * Create a user with a given set of permissions.
|
||
1106 | *
|
||
1107 | * @param array $permissions
|
||
1108 | * Array of permission names to assign to user. Note that the user always
|
||
1109 | * has the default permissions derived from the "authenticated users" role.
|
||
1110 | *
|
||
1111 | * @return object|false
|
||
1112 | * A fully loaded user object with pass_raw property, or FALSE if account
|
||
1113 | * creation fails.
|
||
1114 | */
|
||
1115 | protected function drupalCreateUser(array $permissions = array()) { |
||
1116 | // Create a role with the given permission set, if any.
|
||
1117 | $rid = FALSE; |
||
1118 | if ($permissions) { |
||
1119 | $rid = $this->drupalCreateRole($permissions); |
||
1120 | if (!$rid) { |
||
1121 | return FALSE; |
||
1122 | } |
||
1123 | } |
||
1124 | |||
1125 | // Create a user assigned to that role.
|
||
1126 | $edit = array(); |
||
1127 | $edit['name'] = $this->randomName(); |
||
1128 | $edit['mail'] = $edit['name'] . '@example.com'; |
||
1129 | $edit['pass'] = user_password(); |
||
1130 | $edit['status'] = 1; |
||
1131 | if ($rid) { |
||
1132 | $edit['roles'] = array($rid => $rid); |
||
1133 | } |
||
1134 | |||
1135 | $account = user_save(drupal_anonymous_user(), $edit); |
||
1136 | |||
1137 | $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login')); |
||
1138 | if (empty($account->uid)) { |
||
1139 | return FALSE; |
||
1140 | } |
||
1141 | |||
1142 | // Add the raw password so that we can log in as this user.
|
||
1143 | $account->pass_raw = $edit['pass']; |
||
1144 | return $account; |
||
1145 | } |
||
1146 | |||
1147 | /**
|
||
1148 | 4444412d | Julien Enselme | * Creates a role with specified permissions.
|
1149 | 85ad3d82 | Assos Assos | *
|
1150 | * @param $permissions
|
||
1151 | * Array of permission names to assign to role.
|
||
1152 | * @param $name
|
||
1153 | * (optional) String for the name of the role. Defaults to a random string.
|
||
1154 | * @return
|
||
1155 | * Role ID of newly created role, or FALSE if role creation failed.
|
||
1156 | */
|
||
1157 | protected function drupalCreateRole(array $permissions, $name = NULL) { |
||
1158 | // Generate random name if it was not passed.
|
||
1159 | if (!$name) { |
||
1160 | $name = $this->randomName(); |
||
1161 | } |
||
1162 | |||
1163 | // Check the all the permissions strings are valid.
|
||
1164 | if (!$this->checkPermissions($permissions)) { |
||
1165 | return FALSE; |
||
1166 | } |
||
1167 | |||
1168 | // Create new role.
|
||
1169 | $role = new stdClass(); |
||
1170 | $role->name = $name; |
||
1171 | user_role_save($role);
|
||
1172 | user_role_grant_permissions($role->rid, $permissions); |
||
1173 | |||
1174 | $this->assertTrue(isset($role->rid), t('Created role of name: @name, id: @rid', array('@name' => $name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role')); |
||
1175 | if ($role && !empty($role->rid)) { |
||
1176 | $count = db_query('SELECT COUNT(*) FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchField(); |
||
1177 | $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role')); |
||
1178 | return $role->rid; |
||
1179 | } |
||
1180 | else {
|
||
1181 | return FALSE; |
||
1182 | } |
||
1183 | } |
||
1184 | |||
1185 | /**
|
||
1186 | * Check to make sure that the array of permissions are valid.
|
||
1187 | *
|
||
1188 | * @param $permissions
|
||
1189 | * Permissions to check.
|
||
1190 | * @param $reset
|
||
1191 | * Reset cached available permissions.
|
||
1192 | * @return
|
||
1193 | * TRUE or FALSE depending on whether the permissions are valid.
|
||
1194 | */
|
||
1195 | protected function checkPermissions(array $permissions, $reset = FALSE) { |
||
1196 | $available = &drupal_static(__FUNCTION__); |
||
1197 | |||
1198 | if (!isset($available) || $reset) { |
||
1199 | $available = array_keys(module_invoke_all('permission')); |
||
1200 | } |
||
1201 | |||
1202 | $valid = TRUE; |
||
1203 | foreach ($permissions as $permission) { |
||
1204 | if (!in_array($permission, $available)) { |
||
1205 | $this->fail(t('Invalid permission %permission.', array('%permission' => $permission)), t('Role')); |
||
1206 | $valid = FALSE; |
||
1207 | } |
||
1208 | } |
||
1209 | return $valid; |
||
1210 | } |
||
1211 | |||
1212 | /**
|
||
1213 | * Log in a user with the internal browser.
|
||
1214 | *
|
||
1215 | * If a user is already logged in, then the current user is logged out before
|
||
1216 | * logging in the specified user.
|
||
1217 | *
|
||
1218 | * Please note that neither the global $user nor the passed-in user object is
|
||
1219 | * populated with data of the logged in user. If you need full access to the
|
||
1220 | * user object after logging in, it must be updated manually. If you also need
|
||
1221 | * access to the plain-text password of the user (set by drupalCreateUser()),
|
||
1222 | * e.g. to log in the same user again, then it must be re-assigned manually.
|
||
1223 | * For example:
|
||
1224 | * @code
|
||
1225 | * // Create a user.
|
||
1226 | * $account = $this->drupalCreateUser(array());
|
||
1227 | * $this->drupalLogin($account);
|
||
1228 | * // Load real user object.
|
||
1229 | * $pass_raw = $account->pass_raw;
|
||
1230 | * $account = user_load($account->uid);
|
||
1231 | * $account->pass_raw = $pass_raw;
|
||
1232 | * @endcode
|
||
1233 | *
|
||
1234 | * @param $account
|
||
1235 | * User object representing the user to log in.
|
||
1236 | *
|
||
1237 | * @see drupalCreateUser()
|
||
1238 | */
|
||
1239 | protected function drupalLogin(stdClass $account) { |
||
1240 | if ($this->loggedInUser) { |
||
1241 | $this->drupalLogout();
|
||
1242 | } |
||
1243 | |||
1244 | $edit = array( |
||
1245 | 'name' => $account->name, |
||
1246 | 'pass' => $account->pass_raw |
||
1247 | ); |
||
1248 | $this->drupalPost('user', $edit, t('Log in')); |
||
1249 | |||
1250 | // If a "log out" link appears on the page, it is almost certainly because
|
||
1251 | // the login was successful.
|
||
1252 | $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $account->name)), t('User login')); |
||
1253 | |||
1254 | if ($pass) { |
||
1255 | $this->loggedInUser = $account; |
||
1256 | } |
||
1257 | } |
||
1258 | |||
1259 | /**
|
||
1260 | * Generate a token for the currently logged in user.
|
||
1261 | */
|
||
1262 | protected function drupalGetToken($value = '') { |
||
1263 | $private_key = drupal_get_private_key();
|
||
1264 | return drupal_hmac_base64($value, $this->session_id . $private_key); |
||
1265 | } |
||
1266 | |||
1267 | /*
|
||
1268 | * Logs a user out of the internal browser, then check the login page to confirm logout.
|
||
1269 | */
|
||
1270 | protected function drupalLogout() { |
||
1271 | // Make a request to the logout page, and redirect to the user page, the
|
||
1272 | // idea being if you were properly logged out you should be seeing a login
|
||
1273 | // screen.
|
||
1274 | $this->drupalGet('user/logout'); |
||
1275 | $this->drupalGet('user'); |
||
1276 | $pass = $this->assertField('name', t('Username field found.'), t('Logout')); |
||
1277 | $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout')); |
||
1278 | |||
1279 | if ($pass) { |
||
1280 | $this->loggedInUser = FALSE; |
||
1281 | } |
||
1282 | } |
||
1283 | |||
1284 | /**
|
||
1285 | * Generates a database prefix for running tests.
|
||
1286 | *
|
||
1287 | * The generated database table prefix is used for the Drupal installation
|
||
1288 | * being performed for the test. It is also used as user agent HTTP header
|
||
1289 | * value by the cURL-based browser of DrupalWebTestCase, which is sent
|
||
1290 | * to the Drupal installation of the test. During early Drupal bootstrap, the
|
||
1291 | * user agent HTTP header is parsed, and if it matches, all database queries
|
||
1292 | * use the database table prefix that has been generated here.
|
||
1293 | *
|
||
1294 | * @see DrupalWebTestCase::curlInitialize()
|
||
1295 | * @see drupal_valid_test_ua()
|
||
1296 | * @see DrupalWebTestCase::setUp()
|
||
1297 | */
|
||
1298 | protected function prepareDatabasePrefix() { |
||
1299 | $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); |
||
1300 | |||
1301 | // As soon as the database prefix is set, the test might start to execute.
|
||
1302 | // All assertions as well as the SimpleTest batch operations are associated
|
||
1303 | // with the testId, so the database prefix has to be associated with it.
|
||
1304 | db_update('simpletest_test_id')
|
||
1305 | ->fields(array('last_prefix' => $this->databasePrefix)) |
||
1306 | ->condition('test_id', $this->testId) |
||
1307 | ->execute(); |
||
1308 | } |
||
1309 | |||
1310 | /**
|
||
1311 | * Changes the database connection to the prefixed one.
|
||
1312 | *
|
||
1313 | * @see DrupalWebTestCase::setUp()
|
||
1314 | */
|
||
1315 | protected function changeDatabasePrefix() { |
||
1316 | if (empty($this->databasePrefix)) { |
||
1317 | $this->prepareDatabasePrefix();
|
||
1318 | // If $this->prepareDatabasePrefix() failed to work, return without
|
||
1319 | // setting $this->setupDatabasePrefix to TRUE, so setUp() methods will
|
||
1320 | // know to bail out.
|
||
1321 | if (empty($this->databasePrefix)) { |
||
1322 | return;
|
||
1323 | } |
||
1324 | } |
||
1325 | |||
1326 | // Clone the current connection and replace the current prefix.
|
||
1327 | $connection_info = Database::getConnectionInfo('default'); |
||
1328 | Database::renameConnection('default', 'simpletest_original_default'); |
||
1329 | foreach ($connection_info as $target => $value) { |
||
1330 | $connection_info[$target]['prefix'] = array( |
||
1331 | 'default' => $value['prefix']['default'] . $this->databasePrefix, |
||
1332 | ); |
||
1333 | } |
||
1334 | Database::addConnectionInfo('default', 'default', $connection_info['default']); |
||
1335 | |||
1336 | // Indicate the database prefix was set up correctly.
|
||
1337 | $this->setupDatabasePrefix = TRUE; |
||
1338 | } |
||
1339 | |||
1340 | /**
|
||
1341 | * Prepares the current environment for running the test.
|
||
1342 | *
|
||
1343 | * Backups various current environment variables and resets them, so they do
|
||
1344 | * not interfere with the Drupal site installation in which tests are executed
|
||
1345 | * and can be restored in tearDown().
|
||
1346 | *
|
||
1347 | * Also sets up new resources for the testing environment, such as the public
|
||
1348 | * filesystem and configuration directories.
|
||
1349 | *
|
||
1350 | * @see DrupalWebTestCase::setUp()
|
||
1351 | * @see DrupalWebTestCase::tearDown()
|
||
1352 | */
|
||
1353 | protected function prepareEnvironment() { |
||
1354 | global $user, $language, $conf; |
||
1355 | |||
1356 | // Store necessary current values before switching to prefixed database.
|
||
1357 | $this->originalLanguage = $language; |
||
1358 | $this->originalLanguageDefault = variable_get('language_default'); |
||
1359 | $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); |
||
1360 | $this->originalProfile = drupal_get_profile();
|
||
1361 | $this->originalCleanUrl = variable_get('clean_url', 0); |
||
1362 | $this->originalUser = $user; |
||
1363 | |||
1364 | // Set to English to prevent exceptions from utf8_truncate() from t()
|
||
1365 | // during install if the current language is not 'en'.
|
||
1366 | // The following array/object conversion is copied from language_default().
|
||
1367 | $language = (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => ''); |
||
1368 | |||
1369 | // Save and clean the shutdown callbacks array because it is static cached
|
||
1370 | // and will be changed by the test run. Otherwise it will contain callbacks
|
||
1371 | // from both environments and the testing environment will try to call the
|
||
1372 | // handlers defined by the original one.
|
||
1373 | $callbacks = &drupal_register_shutdown_function();
|
||
1374 | $this->originalShutdownCallbacks = $callbacks; |
||
1375 | $callbacks = array(); |
||
1376 | |||
1377 | // Create test directory ahead of installation so fatal errors and debug
|
||
1378 | // information can be logged during installation process.
|
||
1379 | // Use temporary files directory with the same prefix as the database.
|
||
1380 | $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); |
||
1381 | $this->private_files_directory = $this->public_files_directory . '/private'; |
||
1382 | $this->temp_files_directory = $this->private_files_directory . '/temp'; |
||
1383 | |||
1384 | // Create the directories
|
||
1385 | file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); |
||
1386 | file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY); |
||
1387 | file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); |
||
1388 | $this->generatedTestFiles = FALSE; |
||
1389 | |||
1390 | // Log fatal errors.
|
||
1391 | ini_set('log_errors', 1); |
||
1392 | ini_set('error_log', $this->public_files_directory . '/error.log'); |
||
1393 | |||
1394 | // Set the test information for use in other parts of Drupal.
|
||
1395 | $test_info = &$GLOBALS['drupal_test_info']; |
||
1396 | $test_info['test_run_id'] = $this->databasePrefix; |
||
1397 | $test_info['in_child_site'] = FALSE; |
||
1398 | |||
1399 | // Indicate the environment was set up correctly.
|
||
1400 | $this->setupEnvironment = TRUE; |
||
1401 | } |
||
1402 | |||
1403 | /**
|
||
1404 | * Sets up a Drupal site for running functional and integration tests.
|
||
1405 | *
|
||
1406 | * Generates a random database prefix and installs Drupal with the specified
|
||
1407 | * installation profile in DrupalWebTestCase::$profile into the
|
||
1408 | * prefixed database. Afterwards, installs any additional modules specified by
|
||
1409 | * the test.
|
||
1410 | *
|
||
1411 | * After installation all caches are flushed and several configuration values
|
||
1412 | * are reset to the values of the parent site executing the test, since the
|
||
1413 | * default values may be incompatible with the environment in which tests are
|
||
1414 | * being executed.
|
||
1415 | *
|
||
1416 | * @param ...
|
||
1417 | * List of modules to enable for the duration of the test. This can be
|
||
1418 | * either a single array or a variable number of string arguments.
|
||
1419 | *
|
||
1420 | * @see DrupalWebTestCase::prepareDatabasePrefix()
|
||
1421 | * @see DrupalWebTestCase::changeDatabasePrefix()
|
||
1422 | * @see DrupalWebTestCase::prepareEnvironment()
|
||
1423 | */
|
||
1424 | protected function setUp() { |
||
1425 | global $user, $language, $conf; |
||
1426 | |||
1427 | // Create the database prefix for this test.
|
||
1428 | $this->prepareDatabasePrefix();
|
||
1429 | |||
1430 | // Prepare the environment for running tests.
|
||
1431 | $this->prepareEnvironment();
|
||
1432 | if (!$this->setupEnvironment) { |
||
1433 | return FALSE; |
||
1434 | } |
||
1435 | |||
1436 | // Reset all statics and variables to perform tests in a clean environment.
|
||
1437 | $conf = array(); |
||
1438 | drupal_static_reset(); |
||
1439 | |||
1440 | // Change the database prefix.
|
||
1441 | // All static variables need to be reset before the database prefix is
|
||
1442 | // changed, since DrupalCacheArray implementations attempt to
|
||
1443 | // write back to persistent caches when they are destructed.
|
||
1444 | $this->changeDatabasePrefix();
|
||
1445 | if (!$this->setupDatabasePrefix) { |
||
1446 | return FALSE; |
||
1447 | } |
||
1448 | |||
1449 | // Preset the 'install_profile' system variable, so the first call into
|
||
1450 | // system_rebuild_module_data() (in drupal_install_system()) will register
|
||
1451 | // the test's profile as a module. Without this, the installation profile of
|
||
1452 | // the parent site (executing the test) is registered, and the test
|
||
1453 | // profile's hook_install() and other hook implementations are never invoked.
|
||
1454 | $conf['install_profile'] = $this->profile; |
||
1455 | |||
1456 | // Perform the actual Drupal installation.
|
||
1457 | include_once DRUPAL_ROOT . '/includes/install.inc'; |
||
1458 | drupal_install_system(); |
||
1459 | |||
1460 | $this->preloadRegistry();
|
||
1461 | |||
1462 | // Set path variables.
|
||
1463 | variable_set('file_public_path', $this->public_files_directory); |
||
1464 | variable_set('file_private_path', $this->private_files_directory); |
||
1465 | variable_set('file_temporary_path', $this->temp_files_directory); |
||
1466 | |||
1467 | // Set the 'simpletest_parent_profile' variable to add the parent profile's
|
||
1468 | // search path to the child site's search paths.
|
||
1469 | // @see drupal_system_listing()
|
||
1470 | // @todo This may need to be primed like 'install_profile' above.
|
||
1471 | variable_set('simpletest_parent_profile', $this->originalProfile); |
||
1472 | |||
1473 | // Include the testing profile.
|
||
1474 | variable_set('install_profile', $this->profile); |
||
1475 | $profile_details = install_profile_info($this->profile, 'en'); |
||
1476 | |||
1477 | // Install the modules specified by the testing profile.
|
||
1478 | module_enable($profile_details['dependencies'], FALSE); |
||
1479 | |||
1480 | // Install modules needed for this test. This could have been passed in as
|
||
1481 | // either a single array argument or a variable number of string arguments.
|
||
1482 | // @todo Remove this compatibility layer in Drupal 8, and only accept
|
||
1483 | // $modules as a single array argument.
|
||
1484 | $modules = func_get_args(); |
||
1485 | if (isset($modules[0]) && is_array($modules[0])) { |
||
1486 | $modules = $modules[0]; |
||
1487 | } |
||
1488 | if ($modules) { |
||
1489 | $success = module_enable($modules, TRUE); |
||
1490 | $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules)))); |
||
1491 | } |
||
1492 | |||
1493 | // Run the profile tasks.
|
||
1494 | $install_profile_module_exists = db_query("SELECT 1 FROM {system} WHERE type = 'module' AND name = :name", array( |
||
1495 | ':name' => $this->profile, |
||
1496 | ))->fetchField(); |
||
1497 | if ($install_profile_module_exists) { |
||
1498 | module_enable(array($this->profile), FALSE); |
||
1499 | } |
||
1500 | |||
1501 | // Reset/rebuild all data structures after enabling the modules.
|
||
1502 | $this->resetAll();
|
||
1503 | |||
1504 | // Run cron once in that environment, as install.php does at the end of
|
||
1505 | // the installation process.
|
||
1506 | drupal_cron_run(); |
||
1507 | |||
1508 | // Ensure that the session is not written to the new environment and replace
|
||
1509 | // the global $user session with uid 1 from the new test site.
|
||
1510 | drupal_save_session(FALSE);
|
||
1511 | // Login as uid 1.
|
||
1512 | $user = user_load(1); |
||
1513 | |||
1514 | // Restore necessary variables.
|
||
1515 | variable_set('install_task', 'done'); |
||
1516 | variable_set('clean_url', $this->originalCleanUrl); |
||
1517 | variable_set('site_mail', 'simpletest@example.com'); |
||
1518 | variable_set('date_default_timezone', date_default_timezone_get());
|
||
1519 | |||
1520 | // Set up English language.
|
||
1521 | unset($conf['language_default']); |
||
1522 | $language = language_default();
|
||
1523 | |||
1524 | // Use the test mail class instead of the default mail handler class.
|
||
1525 | variable_set('mail_system', array('default-system' => 'TestingMailSystem')); |
||
1526 | |||
1527 | drupal_set_time_limit($this->timeLimit);
|
||
1528 | $this->setup = TRUE; |
||
1529 | } |
||
1530 | |||
1531 | /**
|
||
1532 | * Preload the registry from the testing site.
|
||
1533 | *
|
||
1534 | * This method is called by DrupalWebTestCase::setUp(), and preloads the
|
||
1535 | * registry from the testing site to cut down on the time it takes to
|
||
1536 | * set up a clean environment for the current test run.
|
||
1537 | */
|
||
1538 | protected function preloadRegistry() { |
||
1539 | // Use two separate queries, each with their own connections: copy the
|
||
1540 | // {registry} and {registry_file} tables over from the parent installation
|
||
1541 | // to the child installation.
|
||
1542 | $original_connection = Database::getConnection('default', 'simpletest_original_default'); |
||
1543 | $test_connection = Database::getConnection(); |
||
1544 | |||
1545 | foreach (array('registry', 'registry_file') as $table) { |
||
1546 | // Find the records from the parent database.
|
||
1547 | $source_query = $original_connection |
||
1548 | ->select($table, array(), array('fetch' => PDO::FETCH_ASSOC)) |
||
1549 | ->fields($table);
|
||
1550 | |||
1551 | $dest_query = $test_connection->insert($table); |
||
1552 | |||
1553 | $first = TRUE; |
||
1554 | foreach ($source_query->execute() as $row) { |
||
1555 | if ($first) { |
||
1556 | $dest_query->fields(array_keys($row)); |
||
1557 | $first = FALSE; |
||
1558 | } |
||
1559 | // Insert the records into the child database.
|
||
1560 | $dest_query->values($row); |
||
1561 | } |
||
1562 | |||
1563 | $dest_query->execute();
|
||
1564 | } |
||
1565 | } |
||
1566 | |||
1567 | /**
|
||
1568 | * Reset all data structures after having enabled new modules.
|
||
1569 | *
|
||
1570 | * This method is called by DrupalWebTestCase::setUp() after enabling
|
||
1571 | * the requested modules. It must be called again when additional modules
|
||
1572 | * are enabled later.
|
||
1573 | */
|
||
1574 | protected function resetAll() { |
||
1575 | // Reset all static variables.
|
||
1576 | drupal_static_reset(); |
||
1577 | // Reset the list of enabled modules.
|
||
1578 | module_list(TRUE);
|
||
1579 | |||
1580 | // Reset cached schema for new database prefix. This must be done before
|
||
1581 | // drupal_flush_all_caches() so rebuilds can make use of the schema of
|
||
1582 | // modules enabled on the cURL side.
|
||
1583 | drupal_get_schema(NULL, TRUE); |
||
1584 | |||
1585 | // Perform rebuilds and flush remaining caches.
|
||
1586 | drupal_flush_all_caches(); |
||
1587 | |||
1588 | // Reload global $conf array and permissions.
|
||
1589 | $this->refreshVariables();
|
||
1590 | $this->checkPermissions(array(), TRUE); |
||
1591 | } |
||
1592 | |||
1593 | /**
|
||
1594 | * Refresh the in-memory set of variables. Useful after a page request is made
|
||
1595 | * that changes a variable in a different thread.
|
||
1596 | *
|
||
1597 | * In other words calling a settings page with $this->drupalPost() with a changed
|
||
1598 | * value would update a variable to reflect that change, but in the thread that
|
||
1599 | * made the call (thread running the test) the changed variable would not be
|
||
1600 | * picked up.
|
||
1601 | *
|
||
1602 | * This method clears the variables cache and loads a fresh copy from the database
|
||
1603 | * to ensure that the most up-to-date set of variables is loaded.
|
||
1604 | */
|
||
1605 | protected function refreshVariables() { |
||
1606 | global $conf; |
||
1607 | cache_clear_all('variables', 'cache_bootstrap'); |
||
1608 | $conf = variable_initialize();
|
||
1609 | } |
||
1610 | |||
1611 | /**
|
||
1612 | * Delete created files and temporary files directory, delete the tables created by setUp(),
|
||
1613 | * and reset the database prefix.
|
||
1614 | */
|
||
1615 | protected function tearDown() { |
||
1616 | global $user, $language; |
||
1617 | |||
1618 | // In case a fatal error occurred that was not in the test process read the
|
||
1619 | // log to pick up any fatal errors.
|
||
1620 | simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); |
||
1621 | |||
1622 | $emailCount = count(variable_get('drupal_test_email_collector', array())); |
||
1623 | if ($emailCount) { |
||
1624 | $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.'); |
||
1625 | $this->pass($message, t('E-mail')); |
||
1626 | } |
||
1627 | |||
1628 | // Delete temporary files directory.
|
||
1629 | file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); |
||
1630 | |||
1631 | // Remove all prefixed tables.
|
||
1632 | $tables = db_find_tables($this->databasePrefix . '%'); |
||
1633 | $connection_info = Database::getConnectionInfo('default'); |
||
1634 | $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%'); |
||
1635 | if (empty($tables)) { |
||
1636 | $this->fail('Failed to find test tables to drop.'); |
||
1637 | } |
||
1638 | $prefix_length = strlen($connection_info['default']['prefix']['default']); |
||
1639 | foreach ($tables as $table) { |
||
1640 | if (db_drop_table(substr($table, $prefix_length))) { |
||
1641 | unset($tables[$table]); |
||
1642 | } |
||
1643 | } |
||
1644 | if (!empty($tables)) { |
||
1645 | $this->fail('Failed to drop all prefixed tables.'); |
||
1646 | } |
||
1647 | |||
1648 | // Get back to the original connection.
|
||
1649 | Database::removeConnection('default'); |
||
1650 | Database::renameConnection('simpletest_original_default', 'default'); |
||
1651 | |||
1652 | // Restore original shutdown callbacks array to prevent original
|
||
1653 | // environment of calling handlers from test run.
|
||
1654 | $callbacks = &drupal_register_shutdown_function();
|
||
1655 | $callbacks = $this->originalShutdownCallbacks; |
||
1656 | |||
1657 | // Return the user to the original one.
|
||
1658 | $user = $this->originalUser; |
||
1659 | drupal_save_session(TRUE);
|
||
1660 | |||
1661 | // Ensure that internal logged in variable and cURL options are reset.
|
||
1662 | $this->loggedInUser = FALSE; |
||
1663 | $this->additionalCurlOptions = array(); |
||
1664 | |||
1665 | // Reload module list and implementations to ensure that test module hooks
|
||
1666 | // aren't called after tests.
|
||
1667 | module_list(TRUE);
|
||
1668 | module_implements('', FALSE, TRUE); |
||
1669 | |||
1670 | // Reset the Field API.
|
||
1671 | field_cache_clear(); |
||
1672 | |||
1673 | // Rebuild caches.
|
||
1674 | $this->refreshVariables();
|
||
1675 | |||
1676 | // Reset public files directory.
|
||
1677 | $GLOBALS['conf']['file_public_path'] = $this->originalFileDirectory; |
||
1678 | |||
1679 | // Reset language.
|
||
1680 | $language = $this->originalLanguage; |
||
1681 | if ($this->originalLanguageDefault) { |
||
1682 | $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; |
||
1683 | } |
||
1684 | |||
1685 | // Close the CURL handler.
|
||
1686 | $this->curlClose();
|
||
1687 | } |
||
1688 | |||
1689 | /**
|
||
1690 | * Initializes the cURL connection.
|
||
1691 | *
|
||
1692 | * If the simpletest_httpauth_credentials variable is set, this function will
|
||
1693 | * add HTTP authentication headers. This is necessary for testing sites that
|
||
1694 | * are protected by login credentials from public access.
|
||
1695 | * See the description of $curl_options for other options.
|
||
1696 | */
|
||
1697 | protected function curlInitialize() { |
||
1698 | global $base_url; |
||
1699 | |||
1700 | if (!isset($this->curlHandle)) { |
||
1701 | $this->curlHandle = curl_init();
|
||
1702 | |||
1703 | // Some versions/configurations of cURL break on a NULL cookie jar, so
|
||
1704 | // supply a real file.
|
||
1705 | if (empty($this->cookieFile)) { |
||
1706 | $this->cookieFile = $this->public_files_directory . '/cookie.jar'; |
||
1707 | } |
||
1708 | |||
1709 | $curl_options = array( |
||
1710 | CURLOPT_COOKIEJAR => $this->cookieFile, |
||
1711 | CURLOPT_URL => $base_url, |
||
1712 | CURLOPT_FOLLOWLOCATION => FALSE, |
||
1713 | CURLOPT_RETURNTRANSFER => TRUE, |
||
1714 | CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on HTTPS. |
||
1715 | CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on HTTPS. |
||
1716 | CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), |
||
1717 | CURLOPT_USERAGENT => $this->databasePrefix, |
||
1718 | ); |
||
1719 | if (isset($this->httpauth_credentials)) { |
||
1720 | $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method; |
||
1721 | $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials; |
||
1722 | } |
||
1723 | // curl_setopt_array() returns FALSE if any of the specified options
|
||
1724 | // cannot be set, and stops processing any further options.
|
||
1725 | $result = curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); |
||
1726 | if (!$result) { |
||
1727 | throw new Exception('One or more cURL options could not be set.'); |
||
1728 | } |
||
1729 | |||
1730 | // By default, the child session name should be the same as the parent.
|
||
1731 | $this->session_name = session_name();
|
||
1732 | } |
||
1733 | // We set the user agent header on each request so as to use the current
|
||
1734 | // time and a new uniqid.
|
||
1735 | if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) { |
||
1736 | curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0])); |
||
1737 | } |
||
1738 | } |
||
1739 | |||
1740 | /**
|
||
1741 | * Initializes and executes a cURL request.
|
||
1742 | *
|
||
1743 | * @param $curl_options
|
||
1744 | * An associative array of cURL options to set, where the keys are constants
|
||
1745 | * defined by the cURL library. For a list of valid options, see
|
||
1746 | * http://www.php.net/manual/function.curl-setopt.php
|
||
1747 | * @param $redirect
|
||
1748 | * FALSE if this is an initial request, TRUE if this request is the result
|
||
1749 | * of a redirect.
|
||
1750 | *
|
||
1751 | * @return
|
||
1752 | * The content returned from the call to curl_exec().
|
||
1753 | *
|
||
1754 | * @see curlInitialize()
|
||
1755 | */
|
||
1756 | protected function curlExec($curl_options, $redirect = FALSE) { |
||
1757 | $this->curlInitialize();
|
||
1758 | |||
1759 | // cURL incorrectly handles URLs with a fragment by including the
|
||
1760 | // fragment in the request to the server, causing some web servers
|
||
1761 | // to reject the request citing "400 - Bad Request". To prevent
|
||
1762 | // this, we strip the fragment from the request.
|
||
1763 | // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0.
|
||
1764 | if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { |
||
1765 | $original_url = $curl_options[CURLOPT_URL]; |
||
1766 | $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); |
||
1767 | } |
||
1768 | |||
1769 | $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; |
||
1770 | |||
1771 | if (!empty($curl_options[CURLOPT_POST])) { |
||
1772 | // This is a fix for the Curl library to prevent Expect: 100-continue
|
||
1773 | // headers in POST requests, that may cause unexpected HTTP response
|
||
1774 | // codes from some webservers (like lighttpd that returns a 417 error
|
||
1775 | // code). It is done by setting an empty "Expect" header field that is
|
||
1776 | // not overwritten by Curl.
|
||
1777 | $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:'; |
||
1778 | } |
||
1779 | curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); |
||
1780 | |||
1781 | if (!$redirect) { |
||
1782 | // Reset headers, the session ID and the redirect counter.
|
||
1783 | $this->session_id = NULL; |
||
1784 | $this->headers = array(); |
||
1785 | $this->redirect_count = 0; |
||
1786 | } |
||
1787 | |||
1788 | $content = curl_exec($this->curlHandle); |
||
1789 | $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); |
||
1790 | |||
1791 | // cURL incorrectly handles URLs with fragments, so instead of
|
||
1792 | // letting cURL handle redirects we take of them ourselves to
|
||
1793 | // to prevent fragments being sent to the web server as part
|
||
1794 | // of the request.
|
||
1795 | // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0.
|
||
1796 | if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < variable_get('simpletest_maximum_redirects', 5)) { |
||
1797 | if ($this->drupalGetHeader('location')) { |
||
1798 | $this->redirect_count++;
|
||
1799 | $curl_options = array(); |
||
1800 | $curl_options[CURLOPT_URL] = $this->drupalGetHeader('location'); |
||
1801 | $curl_options[CURLOPT_HTTPGET] = TRUE; |
||
1802 | return $this->curlExec($curl_options, TRUE); |
||
1803 | } |
||
1804 | } |
||
1805 | |||
1806 | $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL)); |
||
1807 | $message_vars = array( |
||
1808 | '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'), |
||
1809 | '@url' => isset($original_url) ? $original_url : $url, |
||
1810 | '@status' => $status, |
||
1811 | '!length' => format_size(strlen($this->drupalGetContent())) |
||
1812 | ); |
||
1813 | $message = t('!method @url returned @status (!length).', $message_vars); |
||
1814 | $this->assertTrue($this->drupalGetContent() !== FALSE, $message, t('Browser')); |
||
1815 | return $this->drupalGetContent(); |
||
1816 | } |
||
1817 | |||
1818 | /**
|
||
1819 | * Reads headers and registers errors received from the tested site.
|
||
1820 | *
|
||
1821 | * @see _drupal_log_error().
|
||
1822 | *
|
||
1823 | * @param $curlHandler
|
||
1824 | * The cURL handler.
|
||
1825 | * @param $header
|
||
1826 | * An header.
|
||
1827 | */
|
||
1828 | protected function curlHeaderCallback($curlHandler, $header) { |
||
1829 | // Header fields can be extended over multiple lines by preceding each
|
||
1830 | // extra line with at least one SP or HT. They should be joined on receive.
|
||
1831 | // Details are in RFC2616 section 4.
|
||
1832 | if ($header[0] == ' ' || $header[0] == "\t") { |
||
1833 | // Normalize whitespace between chucks.
|
||
1834 | $this->headers[] = array_pop($this->headers) . ' ' . trim($header); |
||
1835 | } |
||
1836 | else {
|
||
1837 | $this->headers[] = $header; |
||
1838 | } |
||
1839 | |||
1840 | // Errors are being sent via X-Drupal-Assertion-* headers,
|
||
1841 | // generated by _drupal_log_error() in the exact form required
|
||
1842 | // by DrupalWebTestCase::error().
|
||
1843 | if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) { |
||
1844 | // Call DrupalWebTestCase::error() with the parameters from the header.
|
||
1845 | call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1]))); |
||
1846 | } |
||
1847 | |||
1848 | // Save cookies.
|
||
1849 | if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) { |
||
1850 | $name = $matches[1]; |
||
1851 | $parts = array_map('trim', explode(';', $matches[2])); |
||
1852 | $value = array_shift($parts); |
||
1853 | $this->cookies[$name] = array('value' => $value, 'secure' => in_array('secure', $parts)); |
||
1854 | if ($name == $this->session_name) { |
||
1855 | if ($value != 'deleted') { |
||
1856 | $this->session_id = $value; |
||
1857 | } |
||
1858 | else {
|
||
1859 | $this->session_id = NULL; |
||
1860 | } |
||
1861 | } |
||
1862 | } |
||
1863 | |||
1864 | // This is required by cURL.
|
||
1865 | return strlen($header); |
||
1866 | } |
||
1867 | |||
1868 | /**
|
||
1869 | * Close the cURL handler and unset the handler.
|
||
1870 | */
|
||
1871 | protected function curlClose() { |
||
1872 | if (isset($this->curlHandle)) { |
||
1873 | curl_close($this->curlHandle);
|
||
1874 | unset($this->curlHandle); |
||
1875 | } |
||
1876 | } |
||
1877 | |||
1878 | /**
|
||
1879 | * Parse content returned from curlExec using DOM and SimpleXML.
|
||
1880 | *
|
||
1881 | * @return
|
||
1882 | * A SimpleXMLElement or FALSE on failure.
|
||
1883 | */
|
||
1884 | protected function parse() { |
||
1885 | if (!$this->elements) { |
||
1886 | // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
|
||
1887 | // them.
|
||
1888 | $htmlDom = new DOMDocument(); |
||
1889 | @$htmlDom->loadHTML($this->drupalGetContent()); |
||
1890 | if ($htmlDom) { |
||
1891 | $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser')); |
||
1892 | // It's much easier to work with simplexml than DOM, luckily enough
|
||
1893 | // we can just simply import our DOM tree.
|
||
1894 | $this->elements = simplexml_import_dom($htmlDom); |
||
1895 | } |
||
1896 | } |
||
1897 | if (!$this->elements) { |
||
1898 | $this->fail(t('Parsed page successfully.'), t('Browser')); |
||
1899 | } |
||
1900 | |||
1901 | return $this->elements; |
||
1902 | } |
||
1903 | |||
1904 | /**
|
||
1905 | * Retrieves a Drupal path or an absolute path.
|
||
1906 | *
|
||
1907 | * @param $path
|
||
1908 | * Drupal path or URL to load into internal browser
|
||
1909 | * @param $options
|
||
1910 | * Options to be forwarded to url().
|
||
1911 | * @param $headers
|
||
1912 | * An array containing additional HTTP request headers, each formatted as
|
||
1913 | * "name: value".
|
||
1914 | * @return
|
||
1915 | * The retrieved HTML string, also available as $this->drupalGetContent()
|
||
1916 | */
|
||
1917 | protected function drupalGet($path, array $options = array(), array $headers = array()) { |
||
1918 | $options['absolute'] = TRUE; |
||
1919 | |||
1920 | // We re-using a CURL connection here. If that connection still has certain
|
||
1921 | // options set, it might change the GET into a POST. Make sure we clear out
|
||
1922 | // previous options.
|
||
1923 | $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); |
||
1924 | $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. |
||
1925 | |||
1926 | // Replace original page output with new output from redirected page(s).
|
||
1927 | if ($new = $this->checkForMetaRefresh()) { |
||
1928 | $out = $new; |
||
1929 | } |
||
1930 | $this->verbose('GET request to: ' . $path . |
||
1931 | '<hr />Ending URL: ' . $this->getUrl() . |
||
1932 | '<hr />' . $out); |
||
1933 | return $out; |
||
1934 | } |
||
1935 | |||
1936 | /**
|
||
1937 | * Retrieve a Drupal path or an absolute path and JSON decode the result.
|
||
1938 | */
|
||
1939 | protected function drupalGetAJAX($path, array $options = array(), array $headers = array()) { |
||
1940 | return drupal_json_decode($this->drupalGet($path, $options, $headers)); |
||
1941 | } |
||
1942 | |||
1943 | /**
|
||
1944 | * Execute a POST request on a Drupal page.
|
||
1945 | * It will be done as usual POST request with SimpleBrowser.
|
||
1946 | *
|
||
1947 | * @param $path
|
||
1948 | * Location of the post form. Either a Drupal path or an absolute path or
|
||
1949 | * NULL to post to the current page. For multi-stage forms you can set the
|
||
1950 | * path to NULL and have it post to the last received page. Example:
|
||
1951 | *
|
||
1952 | * @code
|
||
1953 | * // First step in form.
|
||
1954 | * $edit = array(...);
|
||
1955 | * $this->drupalPost('some_url', $edit, t('Save'));
|
||
1956 | *
|
||
1957 | * // Second step in form.
|
||
1958 | * $edit = array(...);
|
||
1959 | * $this->drupalPost(NULL, $edit, t('Save'));
|
||
1960 | * @endcode
|
||
1961 | * @param $edit
|
||
1962 | * Field data in an associative array. Changes the current input fields
|
||
1963 | * (where possible) to the values indicated. A checkbox can be set to
|
||
1964 | * TRUE to be checked and FALSE to be unchecked. Note that when a form
|
||
1965 | * contains file upload fields, other fields cannot start with the '@'
|
||
1966 | * character.
|
||
1967 | *
|
||
1968 | * Multiple select fields can be set using name[] and setting each of the
|
||
1969 | * possible values. Example:
|
||
1970 | * @code
|
||
1971 | * $edit = array();
|
||
1972 | * $edit['name[]'] = array('value1', 'value2');
|
||
1973 | * @endcode
|
||
1974 | * @param $submit
|
||
1975 | * Value of the submit button whose click is to be emulated. For example,
|
||
1976 | * t('Save'). The processing of the request depends on this value. For
|
||
1977 | * example, a form may have one button with the value t('Save') and another
|
||
1978 | * button with the value t('Delete'), and execute different code depending
|
||
1979 | * on which one is clicked.
|
||
1980 | *
|
||
1981 | * This function can also be called to emulate an Ajax submission. In this
|
||
1982 | * case, this value needs to be an array with the following keys:
|
||
1983 | * - path: A path to submit the form values to for Ajax-specific processing,
|
||
1984 | * which is likely different than the $path parameter used for retrieving
|
||
1985 | * the initial form. Defaults to 'system/ajax'.
|
||
1986 | * - triggering_element: If the value for the 'path' key is 'system/ajax' or
|
||
1987 | * another generic Ajax processing path, this needs to be set to the name
|
||
1988 | * of the element. If the name doesn't identify the element uniquely, then
|
||
1989 | * this should instead be an array with a single key/value pair,
|
||
1990 | * corresponding to the element name and value. The callback for the
|
||
1991 | * generic Ajax processing path uses this to find the #ajax information
|
||
1992 | * for the element, including which specific callback to use for
|
||
1993 | * processing the request.
|
||
1994 | *
|
||
1995 | * This can also be set to NULL in order to emulate an Internet Explorer
|
||
1996 | * submission of a form with a single text field, and pressing ENTER in that
|
||
1997 | * textfield: under these conditions, no button information is added to the
|
||
1998 | * POST data.
|
||
1999 | * @param $options
|
||
2000 | * Options to be forwarded to url().
|
||
2001 | * @param $headers
|
||
2002 | * An array containing additional HTTP request headers, each formatted as
|
||
2003 | * "name: value".
|
||
2004 | * @param $form_html_id
|
||
2005 | * (optional) HTML ID of the form to be submitted. On some pages
|
||
2006 | * there are many identical forms, so just using the value of the submit
|
||
2007 | * button is not enough. For example: 'trigger-node-presave-assign-form'.
|
||
2008 | * Note that this is not the Drupal $form_id, but rather the HTML ID of the
|
||
2009 | * form, which is typically the same thing but with hyphens replacing the
|
||
2010 | * underscores.
|
||
2011 | * @param $extra_post
|
||
2012 | * (optional) A string of additional data to append to the POST submission.
|
||
2013 | * This can be used to add POST data for which there are no HTML fields, as
|
||
2014 | * is done by drupalPostAJAX(). This string is literally appended to the
|
||
2015 | * POST data, so it must already be urlencoded and contain a leading "&"
|
||
2016 | * (e.g., "&extra_var1=hello+world&extra_var2=you%26me").
|
||
2017 | */
|
||
2018 | protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { |
||
2019 | $submit_matches = FALSE; |
||
2020 | $ajax = is_array($submit); |
||
2021 | if (isset($path)) { |
||
2022 | $this->drupalGet($path, $options); |
||
2023 | } |
||
2024 | if ($this->parse()) { |
||
2025 | $edit_save = $edit; |
||
2026 | // Let's iterate over all the forms.
|
||
2027 | $xpath = "//form"; |
||
2028 | if (!empty($form_html_id)) { |
||
2029 | $xpath .= "[@id='" . $form_html_id . "']"; |
||
2030 | } |
||
2031 | $forms = $this->xpath($xpath); |
||
2032 | foreach ($forms as $form) { |
||
2033 | // We try to set the fields of this form as specified in $edit.
|
||
2034 | $edit = $edit_save; |
||
2035 | $post = array(); |
||
2036 | $upload = array(); |
||
2037 | $submit_matches = $this->handleForm($post, $edit, $upload, $ajax ? NULL : $submit, $form); |
||
2038 | $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl(); |
||
2039 | if ($ajax) { |
||
2040 | $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax'); |
||
2041 | // Ajax callbacks verify the triggering element if necessary, so while
|
||
2042 | // we may eventually want extra code that verifies it in the
|
||
2043 | // handleForm() function, it's not currently a requirement.
|
||
2044 | $submit_matches = TRUE; |
||
2045 | } |
||
2046 | |||
2047 | // We post only if we managed to handle every field in edit and the
|
||
2048 | // submit button matches.
|
||
2049 | if (!$edit && ($submit_matches || !isset($submit))) { |
||
2050 | $post_array = $post; |
||
2051 | if ($upload) { |
||
2052 | // TODO: cURL handles file uploads for us, but the implementation
|
||
2053 | // is broken. This is a less than elegant workaround. Alternatives
|
||
2054 | // are being explored at #253506.
|
||
2055 | foreach ($upload as $key => $file) { |
||
2056 | $file = drupal_realpath($file); |
||
2057 | if ($file && is_file($file)) { |
||
2058 | // Use the new CurlFile class for file uploads when using PHP
|
||
2059 | // 5.5 or higher.
|
||
2060 | if (class_exists('CurlFile')) { |
||
2061 | $post[$key] = curl_file_create($file); |
||
2062 | } |
||
2063 | else {
|
||
2064 | $post[$key] = '@' . $file; |
||
2065 | } |
||
2066 | } |
||
2067 | } |
||
2068 | } |
||
2069 | else {
|
||
2070 | foreach ($post as $key => $value) { |
||
2071 | // Encode according to application/x-www-form-urlencoded
|
||
2072 | // Both names and values needs to be urlencoded, according to
|
||
2073 | // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
|
||
2074 | $post[$key] = urlencode($key) . '=' . urlencode($value); |
||
2075 | } |
||
2076 | $post = implode('&', $post) . $extra_post; |
||
2077 | } |
||
2078 | $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers)); |
||
2079 | // Ensure that any changes to variables in the other thread are picked up.
|
||
2080 | $this->refreshVariables();
|
||
2081 | |||
2082 | // Replace original page output with new output from redirected page(s).
|
||
2083 | if ($new = $this->checkForMetaRefresh()) { |
||
2084 | $out = $new; |
||
2085 | } |
||
2086 | $this->verbose('POST request to: ' . $path . |
||
2087 | '<hr />Ending URL: ' . $this->getUrl() . |
||
2088 | '<hr />Fields: ' . highlight_string('<?php ' . var_export($post_array, TRUE), TRUE) . |
||
2089 | '<hr />' . $out); |
||
2090 | return $out; |
||
2091 | } |
||
2092 | } |
||
2093 | // We have not found a form which contained all fields of $edit.
|
||
2094 | foreach ($edit as $name => $value) { |
||
2095 | $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); |
||
2096 | } |
||
2097 | if (!$ajax && isset($submit)) { |
||
2098 | $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit))); |
||
2099 | } |
||
2100 | $this->fail(t('Found the requested form fields at @path', array('@path' => $path))); |
||
2101 | } |
||
2102 | } |
||
2103 | |||
2104 | /**
|
||
2105 | * Execute an Ajax submission.
|
||
2106 | *
|
||
2107 | * This executes a POST as ajax.js does. It uses the returned JSON data, an
|
||
2108 | * array of commands, to update $this->content using equivalent DOM
|
||
2109 | * manipulation as is used by ajax.js. It also returns the array of commands.
|
||
2110 | *
|
||
2111 | * @param $path
|
||
2112 | * Location of the form containing the Ajax enabled element to test. Can be
|
||
2113 | * either a Drupal path or an absolute path or NULL to use the current page.
|
||
2114 | * @param $edit
|
||
2115 | * Field data in an associative array. Changes the current input fields
|
||
2116 | * (where possible) to the values indicated.
|
||
2117 | * @param $triggering_element
|
||
2118 | * The name of the form element that is responsible for triggering the Ajax
|
||
2119 | * functionality to test. May be a string or, if the triggering element is
|
||
2120 | * a button, an associative array where the key is the name of the button
|
||
2121 | * and the value is the button label. i.e.) array('op' => t('Refresh')).
|
||
2122 | * @param $ajax_path
|
||
2123 | * (optional) Override the path set by the Ajax settings of the triggering
|
||
2124 | * element. In the absence of both the triggering element's Ajax path and
|
||
2125 | * $ajax_path 'system/ajax' will be used.
|
||
2126 | * @param $options
|
||
2127 | * (optional) Options to be forwarded to url().
|
||
2128 | * @param $headers
|
||
2129 | * (optional) An array containing additional HTTP request headers, each
|
||
2130 | * formatted as "name: value". Forwarded to drupalPost().
|
||
2131 | * @param $form_html_id
|
||
2132 | * (optional) HTML ID of the form to be submitted, use when there is more
|
||
2133 | * than one identical form on the same page and the value of the triggering
|
||
2134 | * element is not enough to identify the form. Note this is not the Drupal
|
||
2135 | * ID of the form but rather the HTML ID of the form.
|
||
2136 | * @param $ajax_settings
|
||
2137 | * (optional) An array of Ajax settings which if specified will be used in
|
||
2138 | * place of the Ajax settings of the triggering element.
|
||
2139 | *
|
||
2140 | * @return
|
||
2141 | * An array of Ajax commands.
|
||
2142 | *
|
||
2143 | * @see drupalPost()
|
||
2144 | * @see ajax.js
|
||
2145 | */
|
||
2146 | protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) { |
||
2147 | // Get the content of the initial page prior to calling drupalPost(), since
|
||
2148 | // drupalPost() replaces $this->content.
|
||
2149 | if (isset($path)) { |
||
2150 | $this->drupalGet($path, $options); |
||
2151 | } |
||
2152 | $content = $this->content; |
||
2153 | $drupal_settings = $this->drupalSettings; |
||
2154 | |||
2155 | // Get the Ajax settings bound to the triggering element.
|
||
2156 | if (!isset($ajax_settings)) { |
||
2157 | if (is_array($triggering_element)) { |
||
2158 | $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]'; |
||
2159 | } |
||
2160 | else {
|
||
2161 | $xpath = '//*[@name="' . $triggering_element . '"]'; |
||
2162 | } |
||
2163 | if (isset($form_html_id)) { |
||
2164 | $xpath = '//form[@id="' . $form_html_id . '"]' . $xpath; |
||
2165 | } |
||
2166 | $element = $this->xpath($xpath); |
||
2167 | $element_id = (string) $element[0]['id']; |
||
2168 | $ajax_settings = $drupal_settings['ajax'][$element_id]; |
||
2169 | } |
||
2170 | |||
2171 | // Add extra information to the POST data as ajax.js does.
|
||
2172 | $extra_post = ''; |
||
2173 | if (isset($ajax_settings['submit'])) { |
||
2174 | foreach ($ajax_settings['submit'] as $key => $value) { |
||
2175 | $extra_post .= '&' . urlencode($key) . '=' . urlencode($value); |
||
2176 | } |
||
2177 | } |
||
2178 | foreach ($this->xpath('//*[@id]') as $element) { |
||
2179 | $id = (string) $element['id']; |
||
2180 | $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id); |
||
2181 | } |
||
2182 | if (isset($drupal_settings['ajaxPageState'])) { |
||
2183 | $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']); |
||
2184 | $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']); |
||
2185 | foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) { |
||
2186 | $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1'; |
||
2187 | } |
||
2188 | foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) { |
||
2189 | $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1'; |
||
2190 | } |
||
2191 | } |
||
2192 | |||
2193 | // Unless a particular path is specified, use the one specified by the
|
||
2194 | // Ajax settings, or else 'system/ajax'.
|
||
2195 | if (!isset($ajax_path)) { |
||
2196 | $ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax'; |
||
2197 | } |
||
2198 | |||
2199 | // Submit the POST request.
|
||
2200 | $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); |
||
2201 | |||
2202 | // Change the page content by applying the returned commands.
|
||
2203 | if (!empty($ajax_settings) && !empty($return)) { |
||
2204 | // ajax.js applies some defaults to the settings object, so do the same
|
||
2205 | // for what's used by this function.
|
||
2206 | $ajax_settings += array( |
||
2207 | 'method' => 'replaceWith', |
||
2208 | ); |
||
2209 | // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
|
||
2210 | // them.
|
||
2211 | $dom = new DOMDocument(); |
||
2212 | @$dom->loadHTML($content); |
||
2213 | // XPath allows for finding wrapper nodes better than DOM does.
|
||
2214 | $xpath = new DOMXPath($dom); |
||
2215 | foreach ($return as $command) { |
||
2216 | switch ($command['command']) { |
||
2217 | case 'settings': |
||
2218 | $drupal_settings = drupal_array_merge_deep($drupal_settings, $command['settings']); |
||
2219 | break;
|
||
2220 | |||
2221 | case 'insert': |
||
2222 | $wrapperNode = NULL; |
||
2223 | // When a command doesn't specify a selector, use the
|
||
2224 | // #ajax['wrapper'] which is always an HTML ID.
|
||
2225 | if (!isset($command['selector'])) { |
||
2226 | $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); |
||
2227 | } |
||
2228 | // @todo Ajax commands can target any jQuery selector, but these are
|
||
2229 | // hard to fully emulate with XPath. For now, just handle 'head'
|
||
2230 | // and 'body', since these are used by ajax_render().
|
||
2231 | elseif (in_array($command['selector'], array('head', 'body'))) { |
||
2232 | $wrapperNode = $xpath->query('//' . $command['selector'])->item(0); |
||
2233 | } |
||
2234 | if ($wrapperNode) { |
||
2235 | // ajax.js adds an enclosing DIV to work around a Safari bug.
|
||
2236 | $newDom = new DOMDocument(); |
||
2237 | $newDom->loadHTML('<div>' . $command['data'] . '</div>'); |
||
2238 | $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); |
||
2239 | $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; |
||
2240 | // The "method" is a jQuery DOM manipulation function. Emulate
|
||
2241 | // each one using PHP's DOMNode API.
|
||
2242 | switch ($method) { |
||
2243 | case 'replaceWith': |
||
2244 | $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); |
||
2245 | break;
|
||
2246 | case 'append': |
||
2247 | $wrapperNode->appendChild($newNode); |
||
2248 | break;
|
||
2249 | case 'prepend': |
||
2250 | // If no firstChild, insertBefore() falls back to
|
||
2251 | // appendChild().
|
||
2252 | $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); |
||
2253 | break;
|
||
2254 | case 'before': |
||
2255 | $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); |
||
2256 | break;
|
||
2257 | case 'after': |
||
2258 | // If no nextSibling, insertBefore() falls back to
|
||
2259 | // appendChild().
|
||
2260 | $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); |
||
2261 | break;
|
||
2262 | case 'html': |
||
2263 | foreach ($wrapperNode->childNodes as $childNode) { |
||
2264 | $wrapperNode->removeChild($childNode); |
||
2265 | } |
||
2266 | $wrapperNode->appendChild($newNode); |
||
2267 | break;
|
||
2268 | } |
||
2269 | } |
||
2270 | break;
|
||
2271 | |||
2272 | 42e6daf3 | Julien Enselme | case 'updateBuildId': |
2273 | $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0); |
||
2274 | if ($buildId) { |
||
2275 | $buildId->setAttribute('value', $command['new']); |
||
2276 | } |
||
2277 | break;
|
||
2278 | |||
2279 | 85ad3d82 | Assos Assos | // @todo Add suitable implementations for these commands in order to
|
2280 | // have full test coverage of what ajax.js can do.
|
||
2281 | case 'remove': |
||
2282 | break;
|
||
2283 | case 'changed': |
||
2284 | break;
|
||
2285 | case 'css': |
||
2286 | break;
|
||
2287 | case 'data': |
||
2288 | break;
|
||
2289 | case 'restripe': |
||
2290 | break;
|
||
2291 | } |
||
2292 | } |
||
2293 | $content = $dom->saveHTML(); |
||
2294 | } |
||
2295 | $this->drupalSetContent($content); |
||
2296 | $this->drupalSetSettings($drupal_settings); |
||
2297 | 4444412d | Julien Enselme | |
2298 | $verbose = 'AJAX POST request to: ' . $path; |
||
2299 | $verbose .= '<br />AJAX callback path: ' . $ajax_path; |
||
2300 | $verbose .= '<hr />Ending URL: ' . $this->getUrl(); |
||
2301 | $verbose .= '<hr />' . $this->content; |
||
2302 | |||
2303 | $this->verbose($verbose); |
||
2304 | |||
2305 | 85ad3d82 | Assos Assos | return $return; |
2306 | } |
||
2307 | |||
2308 | /**
|
||
2309 | * Runs cron in the Drupal installed by Simpletest.
|
||
2310 | */
|
||
2311 | protected function cronRun() { |
||
2312 | $this->drupalGet($GLOBALS['base_url'] . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => variable_get('cron_key', 'drupal')))); |
||
2313 | } |
||
2314 | |||
2315 | /**
|
||
2316 | * Check for meta refresh tag and if found call drupalGet() recursively. This
|
||
2317 | * function looks for the http-equiv attribute to be set to "Refresh"
|
||
2318 | * and is case-sensitive.
|
||
2319 | *
|
||
2320 | * @return
|
||
2321 | * Either the new page content or FALSE.
|
||
2322 | */
|
||
2323 | protected function checkForMetaRefresh() { |
||
2324 | if (strpos($this->drupalGetContent(), '<meta ') && $this->parse()) { |
||
2325 | $refresh = $this->xpath('//meta[@http-equiv="Refresh"]'); |
||
2326 | if (!empty($refresh)) { |
||
2327 | // Parse the content attribute of the meta tag for the format:
|
||
2328 | // "[delay]: URL=[page_to_redirect_to]".
|
||
2329 | if (preg_match('/\d+;\s*URL=(?P<url>.*)/i', $refresh[0]['content'], $match)) { |
||
2330 | return $this->drupalGet($this->getAbsoluteUrl(decode_entities($match['url']))); |
||
2331 | } |
||
2332 | } |
||
2333 | } |
||
2334 | return FALSE; |
||
2335 | } |
||
2336 | |||
2337 | /**
|
||
2338 | * Retrieves only the headers for a Drupal path or an absolute path.
|
||
2339 | *
|
||
2340 | * @param $path
|
||
2341 | * Drupal path or URL to load into internal browser
|
||
2342 | * @param $options
|
||
2343 | * Options to be forwarded to url().
|
||
2344 | * @param $headers
|
||
2345 | * An array containing additional HTTP request headers, each formatted as
|
||
2346 | * "name: value".
|
||
2347 | * @return
|
||
2348 | * The retrieved headers, also available as $this->drupalGetContent()
|
||
2349 | */
|
||
2350 | protected function drupalHead($path, array $options = array(), array $headers = array()) { |
||
2351 | $options['absolute'] = TRUE; |
||
2352 | $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers)); |
||
2353 | $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. |
||
2354 | return $out; |
||
2355 | } |
||
2356 | |||
2357 | /**
|
||
2358 | * Handle form input related to drupalPost(). Ensure that the specified fields
|
||
2359 | * exist and attempt to create POST data in the correct manner for the particular
|
||
2360 | * field type.
|
||
2361 | *
|
||
2362 | * @param $post
|
||
2363 | * Reference to array of post values.
|
||
2364 | * @param $edit
|
||
2365 | * Reference to array of edit values to be checked against the form.
|
||
2366 | * @param $submit
|
||
2367 | * Form submit button value.
|
||
2368 | * @param $form
|
||
2369 | * Array of form elements.
|
||
2370 | * @return
|
||
2371 | * Submit value matches a valid submit input in the form.
|
||
2372 | */
|
||
2373 | protected function handleForm(&$post, &$edit, &$upload, $submit, $form) { |
||
2374 | // Retrieve the form elements.
|
||
2375 | $elements = $form->xpath('.//input[not(@disabled)]|.//textarea[not(@disabled)]|.//select[not(@disabled)]'); |
||
2376 | $submit_matches = FALSE; |
||
2377 | foreach ($elements as $element) { |
||
2378 | // SimpleXML objects need string casting all the time.
|
||
2379 | $name = (string) $element['name']; |
||
2380 | // This can either be the type of <input> or the name of the tag itself
|
||
2381 | // for <select> or <textarea>.
|
||
2382 | $type = isset($element['type']) ? (string) $element['type'] : $element->getName(); |
||
2383 | $value = isset($element['value']) ? (string) $element['value'] : ''; |
||
2384 | $done = FALSE; |
||
2385 | if (isset($edit[$name])) { |
||
2386 | switch ($type) { |
||
2387 | case 'text': |
||
2388 | case 'tel': |
||
2389 | case 'textarea': |
||
2390 | case 'url': |
||
2391 | case 'number': |
||
2392 | case 'range': |
||
2393 | case 'color': |
||
2394 | case 'hidden': |
||
2395 | case 'password': |
||
2396 | case 'email': |
||
2397 | case 'search': |
||
2398 | $post[$name] = $edit[$name]; |
||
2399 | unset($edit[$name]); |
||
2400 | break;
|
||
2401 | case 'radio': |
||
2402 | if ($edit[$name] == $value) { |
||
2403 | $post[$name] = $edit[$name]; |
||
2404 | unset($edit[$name]); |
||
2405 | } |
||
2406 | break;
|
||
2407 | case 'checkbox': |
||
2408 | // To prevent checkbox from being checked.pass in a FALSE,
|
||
2409 | // otherwise the checkbox will be set to its value regardless
|
||
2410 | // of $edit.
|
||
2411 | if ($edit[$name] === FALSE) { |
||
2412 | unset($edit[$name]); |
||
2413 | continue 2; |
||
2414 | } |
||
2415 | else {
|
||
2416 | unset($edit[$name]); |
||
2417 | $post[$name] = $value; |
||
2418 | } |
||
2419 | break;
|
||
2420 | case 'select': |
||
2421 | $new_value = $edit[$name]; |
||
2422 | $options = $this->getAllOptions($element); |
||
2423 | if (is_array($new_value)) { |
||
2424 | // Multiple select box.
|
||
2425 | if (!empty($new_value)) { |
||
2426 | $index = 0; |
||
2427 | $key = preg_replace('/\[\]$/', '', $name); |
||
2428 | foreach ($options as $option) { |
||
2429 | $option_value = (string) $option['value']; |
||
2430 | if (in_array($option_value, $new_value)) { |
||
2431 | $post[$key . '[' . $index++ . ']'] = $option_value; |
||
2432 | $done = TRUE; |
||
2433 | unset($edit[$name]); |
||
2434 | } |
||
2435 | } |
||
2436 | } |
||
2437 | else {
|
||
2438 | // No options selected: do not include any POST data for the
|
||
2439 | // element.
|
||
2440 | $done = TRUE; |
||
2441 | unset($edit[$name]); |
||
2442 | } |
||
2443 | } |
||
2444 | else {
|
||
2445 | // Single select box.
|
||
2446 | foreach ($options as $option) { |
||
2447 | if ($new_value == $option['value']) { |
||
2448 | $post[$name] = $new_value; |
||
2449 | unset($edit[$name]); |
||
2450 | $done = TRUE; |
||
2451 | break;
|
||
2452 | } |
||
2453 | } |
||
2454 | } |
||
2455 | break;
|
||
2456 | case 'file': |
||
2457 | $upload[$name] = $edit[$name]; |
||
2458 | unset($edit[$name]); |
||
2459 | break;
|
||
2460 | } |
||
2461 | } |
||
2462 | if (!isset($post[$name]) && !$done) { |
||
2463 | switch ($type) { |
||
2464 | case 'textarea': |
||
2465 | $post[$name] = (string) $element; |
||
2466 | break;
|
||
2467 | case 'select': |
||
2468 | $single = empty($element['multiple']); |
||
2469 | $first = TRUE; |
||
2470 | $index = 0; |
||
2471 | $key = preg_replace('/\[\]$/', '', $name); |
||
2472 | $options = $this->getAllOptions($element); |
||
2473 | foreach ($options as $option) { |
||
2474 | // For single select, we load the first option, if there is a
|
||
2475 | // selected option that will overwrite it later.
|
||
2476 | if ($option['selected'] || ($first && $single)) { |
||
2477 | $first = FALSE; |
||
2478 | if ($single) { |
||
2479 | $post[$name] = (string) $option['value']; |
||
2480 | } |
||
2481 | else {
|
||
2482 | $post[$key . '[' . $index++ . ']'] = (string) $option['value']; |
||
2483 | } |
||
2484 | } |
||
2485 | } |
||
2486 | break;
|
||
2487 | case 'file': |
||
2488 | break;
|
||
2489 | case 'submit': |
||
2490 | case 'image': |
||
2491 | if (isset($submit) && $submit == $value) { |
||
2492 | $post[$name] = $value; |
||
2493 | $submit_matches = TRUE; |
||
2494 | } |
||
2495 | break;
|
||
2496 | case 'radio': |
||
2497 | case 'checkbox': |
||
2498 | if (!isset($element['checked'])) { |
||
2499 | break;
|
||
2500 | } |
||
2501 | // Deliberate no break.
|
||
2502 | default:
|
||
2503 | $post[$name] = $value; |
||
2504 | } |
||
2505 | } |
||
2506 | } |
||
2507 | return $submit_matches; |
||
2508 | } |
||
2509 | |||
2510 | /**
|
||
2511 | * Builds an XPath query.
|
||
2512 | *
|
||
2513 | * Builds an XPath query by replacing placeholders in the query by the value
|
||
2514 | * of the arguments.
|
||
2515 | *
|
||
2516 | * XPath 1.0 (the version supported by libxml2, the underlying XML library
|
||
2517 | * used by PHP) doesn't support any form of quotation. This function
|
||
2518 | * simplifies the building of XPath expression.
|
||
2519 | *
|
||
2520 | * @param $xpath
|
||
2521 | * An XPath query, possibly with placeholders in the form ':name'.
|
||
2522 | * @param $args
|
||
2523 | * An array of arguments with keys in the form ':name' matching the
|
||
2524 | * placeholders in the query. The values may be either strings or numeric
|
||
2525 | * values.
|
||
2526 | * @return
|
||
2527 | * An XPath query with arguments replaced.
|
||
2528 | */
|
||
2529 | protected function buildXPathQuery($xpath, array $args = array()) { |
||
2530 | // Replace placeholders.
|
||
2531 | foreach ($args as $placeholder => $value) { |
||
2532 | // XPath 1.0 doesn't support a way to escape single or double quotes in a
|
||
2533 | // string literal. We split double quotes out of the string, and encode
|
||
2534 | // them separately.
|
||
2535 | if (is_string($value)) { |
||
2536 | // Explode the text at the quote characters.
|
||
2537 | $parts = explode('"', $value); |
||
2538 | |||
2539 | // Quote the parts.
|
||
2540 | foreach ($parts as &$part) { |
||
2541 | $part = '"' . $part . '"'; |
||
2542 | } |
||
2543 | |||
2544 | // Return the string.
|
||
2545 | $value = count($parts) > 1 ? 'concat(' . implode(', \'"\', ', $parts) . ')' : $parts[0]; |
||
2546 | } |
||
2547 | $xpath = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $xpath); |
||
2548 | } |
||
2549 | return $xpath; |
||
2550 | } |
||
2551 | |||
2552 | /**
|
||
2553 | * Perform an xpath search on the contents of the internal browser. The search
|
||
2554 | * is relative to the root element (HTML tag normally) of the page.
|
||
2555 | *
|
||
2556 | * @param $xpath
|
||
2557 | * The xpath string to use in the search.
|
||
2558 | * @return
|
||
2559 | * The return value of the xpath search. For details on the xpath string
|
||
2560 | * format and return values see the SimpleXML documentation,
|
||
2561 | * http://us.php.net/manual/function.simplexml-element-xpath.php.
|
||
2562 | */
|
||
2563 | protected function xpath($xpath, array $arguments = array()) { |
||
2564 | if ($this->parse()) { |
||
2565 | $xpath = $this->buildXPathQuery($xpath, $arguments); |
||
2566 | $result = $this->elements->xpath($xpath); |
||
2567 | // Some combinations of PHP / libxml versions return an empty array
|
||
2568 | // instead of the documented FALSE. Forcefully convert any falsish values
|
||
2569 | // to an empty array to allow foreach(...) constructions.
|
||
2570 | return $result ? $result : array(); |
||
2571 | } |
||
2572 | else {
|
||
2573 | return FALSE; |
||
2574 | } |
||
2575 | } |
||
2576 | |||
2577 | /**
|
||
2578 | * Get all option elements, including nested options, in a select.
|
||
2579 | *
|
||
2580 | * @param $element
|
||
2581 | * The element for which to get the options.
|
||
2582 | * @return
|
||
2583 | * Option elements in select.
|
||
2584 | */
|
||
2585 | protected function getAllOptions(SimpleXMLElement $element) { |
||
2586 | $options = array(); |
||
2587 | // Add all options items.
|
||
2588 | foreach ($element->option as $option) { |
||
2589 | $options[] = $option; |
||
2590 | } |
||
2591 | |||
2592 | // Search option group children.
|
||
2593 | if (isset($element->optgroup)) { |
||
2594 | foreach ($element->optgroup as $group) { |
||
2595 | $options = array_merge($options, $this->getAllOptions($group)); |
||
2596 | } |
||
2597 | } |
||
2598 | return $options; |
||
2599 | } |
||
2600 | |||
2601 | /**
|
||
2602 | * Pass if a link with the specified label is found, and optional with the
|
||
2603 | * specified index.
|
||
2604 | *
|
||
2605 | * @param $label
|
||
2606 | * Text between the anchor tags.
|
||
2607 | * @param $index
|
||
2608 | * Link position counting from zero.
|
||
2609 | * @param $message
|
||
2610 | * Message to display.
|
||
2611 | * @param $group
|
||
2612 | * The group this message belongs to, defaults to 'Other'.
|
||
2613 | * @return
|
||
2614 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
2615 | */
|
||
2616 | protected function assertLink($label, $index = 0, $message = '', $group = 'Other') { |
||
2617 | $links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label)); |
||
2618 | $message = ($message ? $message : t('Link with label %label found.', array('%label' => $label))); |
||
2619 | return $this->assert(isset($links[$index]), $message, $group); |
||
2620 | } |
||
2621 | |||
2622 | /**
|
||
2623 | * Pass if a link with the specified label is not found.
|
||
2624 | *
|
||
2625 | * @param $label
|
||
2626 | * Text between the anchor tags.
|
||
2627 | * @param $message
|
||
2628 | * Message to display.
|
||
2629 | * @param $group
|
||
2630 | * The group this message belongs to, defaults to 'Other'.
|
||
2631 | * @return
|
||
2632 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
2633 | */
|
||
2634 | protected function assertNoLink($label, $message = '', $group = 'Other') { |
||
2635 | $links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label)); |
||
2636 | $message = ($message ? $message : t('Link with label %label not found.', array('%label' => $label))); |
||
2637 | return $this->assert(empty($links), $message, $group); |
||
2638 | } |
||
2639 | |||
2640 | /**
|
||
2641 | * Pass if a link containing a given href (part) is found.
|
||
2642 | *
|
||
2643 | * @param $href
|
||
2644 | * The full or partial value of the 'href' attribute of the anchor tag.
|
||
2645 | * @param $index
|
||
2646 | * Link position counting from zero.
|
||
2647 | * @param $message
|
||
2648 | * Message to display.
|
||
2649 | * @param $group
|
||
2650 | * The group this message belongs to, defaults to 'Other'.
|
||
2651 | *
|
||
2652 | * @return
|
||
2653 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
2654 | */
|
||
2655 | protected function assertLinkByHref($href, $index = 0, $message = '', $group = 'Other') { |
||
2656 | $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href)); |
||
2657 | $message = ($message ? $message : t('Link containing href %href found.', array('%href' => $href))); |
||
2658 | return $this->assert(isset($links[$index]), $message, $group); |
||
2659 | } |
||
2660 | |||
2661 | /**
|
||
2662 | * Pass if a link containing a given href (part) is not found.
|
||
2663 | *
|
||
2664 | * @param $href
|
||
2665 | * The full or partial value of the 'href' attribute of the anchor tag.
|
||
2666 | * @param $message
|
||
2667 | * Message to display.
|
||
2668 | * @param $group
|
||
2669 | * The group this message belongs to, defaults to 'Other'.
|
||
2670 | *
|
||
2671 | * @return
|
||
2672 | * TRUE if the assertion succeeded, FALSE otherwise.
|
||
2673 | */
|
||
2674 | protected function assertNoLinkByHref($href, $message = '', $group = 'Other') { |
||
2675 | $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href)); |
||
2676 | $message = ($message ? $message : t('No link containing href %href found.', array('%href' => $href))); |
||
2677 | return $this->assert(empty($links), $message, $group); |
||
2678 | } |
||
2679 | |||
2680 | /**
|
||
2681 | * Follows a link by name.
|
||
2682 | *
|
||
2683 | * Will click the first link found with this link text by default, or a later
|
||
2684 | * one if an index is given. Match is case sensitive with normalized space.
|
||
2685 | * The label is translated label. There is an assert for successful click.
|
||
2686 | *
|
||
2687 | * @param $label
|
||
2688 | * Text between the anchor tags.
|
||
2689 | * @param $index
|
||
2690 | * Link position counting from zero.
|
||
2691 | * @return
|
||
2692 | * Page on success, or FALSE on failure.
|
||
2693 | */
|
||
2694 | protected function clickLink($label, $index = 0) { |
||
2695 | $url_before = $this->getUrl(); |
||
2696 | $urls = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label)); |
||
2697 | |||
2698 | if (isset($urls[$index])) { |
||
2699 | $url_target = $this->getAbsoluteUrl($urls[$index]['href']); |
||
2700 | } |
||
2701 | |||
2702 | $this->assertTrue(isset($urls[$index]), t('Clicked link %label (@url_target) from @url_before', array('%label' => $label, '@url_target' => $url_target, '@url_before' => $url_before)), t('Browser')); |
||
2703 | |||
2704 | if (isset($url_target)) { |
||
2705 | return $this->drupalGet($url_target); |
||
2706 | } |
||
2707 | return FALSE; |
||
2708 | } |
||
2709 | |||
2710 | /**
|
||
2711 | * Takes a path and returns an absolute path.
|
||
2712 | *
|
||
2713 | * @param $path
|
||
2714 | * A path from the internal browser content.
|
||
2715 | * @return
|
||
2716 | * The $path with $base_url prepended, if necessary.
|
||
2717 | */
|
||
2718 | protected function getAbsoluteUrl($path) { |
||
2719 | global $base_url, $base_path; |
||
2720 | |||
2721 | $parts = parse_url($path); |
||
2722 | if (empty($parts['host'])) { |
||
2723 | // Ensure that we have a string (and no xpath object).
|
||
2724 | $path = (string) $path; |
||
2725 | // Strip $base_path, if existent.
|
||
2726 | $length = strlen($base_path); |
||
2727 | if (substr($path, 0, $length) === $base_path) { |
||
2728 | $path = substr($path, $length); |
||
2729 | } |
||
2730 | // Ensure that we have an absolute path.
|
||
2731 | if ($path[0] !== '/') { |
||
2732 | $path = '/' . $path; |
||
2733 | } |
||
2734 | // Finally, prepend the $base_url.
|
||
2735 | $path = $base_url . $path; |
||
2736 | } |
||
2737 | return $path; |
||
2738 | } |
||
2739 | |||
2740 | /**
|
||
2741 | * Get the current URL from the cURL handler.
|
||
2742 | *
|
||
2743 | * @return
|
||
2744 | * The current URL.
|
||
2745 | */
|
||
2746 | protected function getUrl() { |
||
2747 | return $this->url; |
||
2748 | } |
||
2749 | |||
2750 | /**
|
||
2751 | * Gets the HTTP response headers of the requested page. Normally we are only
|
||
2752 | * interested in the headers returned by the last request. However, if a page
|
||
2753 | * is redirected or HTTP authentication is in use, multiple requests will be
|
||
2754 | * required to retrieve the page. Headers from all requests may be requested
|
||
2755 | * by passing TRUE to this function.
|
||
2756 | *
|
||
2757 | * @param $all_requests
|
||
2758 | * Boolean value specifying whether to return headers from all requests
|
||
2759 | * instead of just the last request. Defaults to FALSE.
|
||
2760 | * @return
|
||
2761 | * A name/value array if headers from only the last request are requested.
|
||
2762 | * If headers from all requests are requested, an array of name/value
|
||
2763 | * arrays, one for each request.
|
||
2764 | *
|
||
2765 | * The pseudonym ":status" is used for the HTTP status line.
|
||
2766 | *
|
||
2767 | * Values for duplicate headers are stored as a single comma-separated list.
|
||
2768 | */
|
||
2769 | protected function drupalGetHeaders($all_requests = FALSE) { |
||
2770 | $request = 0; |
||
2771 | $headers = array($request => array()); |
||
2772 | foreach ($this->headers as $header) { |
||
2773 | $header = trim($header); |
||
2774 | if ($header === '') { |
||
2775 | $request++;
|
||
2776 | } |
||
2777 | else {
|
||
2778 | if (strpos($header, 'HTTP/') === 0) { |
||
2779 | $name = ':status'; |
||
2780 | $value = $header; |
||
2781 | } |
||
2782 | else {
|
||
2783 | list($name, $value) = explode(':', $header, 2); |
||
2784 | $name = strtolower($name); |
||
2785 | } |
||
2786 | if (isset($headers[$request][$name])) { |
||
2787 | $headers[$request][$name] .= ',' . trim($value); |
||
2788 | } |
||
2789 | else {
|
||
2790 | $headers[$request][$name] = trim($value); |
||
2791 | } |
||
2792 | } |
||
2793 | } |
||
2794 | if (!$all_requests) { |
||
2795 | $headers = array_pop($headers); |
||
2796 | } |
||
2797 | return $headers; |
||
2798 | } |
||
2799 | |||
2800 | /**
|
||
2801 | * Gets the value of an HTTP response header. If multiple requests were
|
||
2802 | * required to retrieve the page, only the headers from the last request will
|
||
2803 | * be checked by default. However, if TRUE is passed as the second argument,
|
||
2804 | * all requests will be processed from last to first until the header is
|
||
2805 | * found.
|
||
2806 | *
|
||
2807 | * @param $name
|
||
2808 | * The name of the header to retrieve. Names are case-insensitive (see RFC
|
||
2809 | * 2616 section 4.2).
|
||
2810 | * @param $all_requests
|
||
2811 | * Boolean value specifying whether to check all requests if the header is
|
||
2812 | * not found in the last request. Defaults to FALSE.
|
||
2813 | * @return
|
||
2814 | * The HTTP header value or FALSE if not found.
|
||
2815 | */
|
||
2816 | protected function drupalGetHeader($name, $all_requests = FALSE) { |
||
2817 | $name = strtolower($name); |
||
2818 | $header = FALSE; |
||
2819 | if ($all_requests) { |
||
2820 | foreach (array_reverse($this->drupalGetHeaders(TRUE)) as $headers) { |
||
2821 | if (isset($headers[$name])) { |
||
2822 | $header = $headers[$name]; |
||
2823 | break;
|
||
2824 | } |
||
2825 | } |
||
2826 | } |
||
2827 | else {
|
||
2828 | $headers = $this->drupalGetHeaders(); |
||
2829 | if (isset($headers[$name])) { |
||
2830 | $header = $headers[$name]; |
||
2831 | } |
||
2832 | } |
||
2833 | return $header; |
||
2834 | } |
||
2835 | |||
2836 | /**
|
||
2837 | * Gets the current raw HTML of requested page.
|
||
2838 | */
|
||
2839 | protected function drupalGetContent() { |
||
2840 | return $this->content; |
||
2841 | } |
||
2842 | |||
2843 | /**
|
||
2844 | * Gets the value of the Drupal.settings JavaScript variable for the currently loaded page.
|
||
2845 | */
|
||
2846 | protected function drupalGetSettings() { |
||
2847 | return $this->drupalSettings; |
||
2848 | } |
||
2849 | |||
2850 | /**
|
||
2851 | * Gets an array containing all e-mails sent during this test case.
|
||
2852 | *
|
||
2853 | * @param $filter
|
||
2854 | * An array containing key/value pairs used to filter the e-mails that are returned.
|
||
2855 | * @return
|
||
2856 | * An array containing e-mail messages captured during the current test.
|
||
2857 | */
|
||
2858 | protected function drupalGetMails($filter = array()) { |
||
2859 | $captured_emails = variable_get('drupal_test_email_collector', array()); |
||
2860 | $filtered_emails = array(); |
||
2861 | |||
2862 | foreach ($captured_emails as $message) { |
||
2863 | foreach ($filter as $key => $value) { |
||
2864 | if (!isset($message[$key]) || $message[$key] != $value) { |
||
2865 | continue 2; |
||
2866 | } |
||
2867 | } |
||
2868 | $filtered_emails[] = $message; |
||
2869 | } |
||
2870 | |||
2871 | return $filtered_emails; |
||
2872 | } |
||
2873 | |||
2874 | /**
|
||
2875 | * Sets the raw HTML content. This can be useful when a page has been fetched
|
||
2876 | * outside of the internal browser and assertions need to be made on the
|
||
2877 | * returned page.
|
||
2878 | *
|
||
2879 | * A good example would be when testing drupal_http_request(). After fetching
|
||
2880 | * the page the content can be set and page elements can be checked to ensure
|
||
2881 | * that the function worked properly.
|
||
2882 | */
|
||
2883 | protected function drupalSetContent($content, $url = 'internal:') { |
||
2884 | $this->content = $content; |
||
2885 | $this->url = $url; |
||
2886 | $this->plainTextContent = FALSE; |
||
2887 | $this->elements = FALSE; |
||
2888 | $this->drupalSettings = array(); |
||
2889 | if (preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $content, $matches)) { |
||
2890 | $this->drupalSettings = drupal_json_decode($matches[1]); |
||
2891 | } |
||
2892 | } |
||
2893 | |||
2894 | /**
|
||
2895 | * Sets the value of the Drupal.settings JavaScript variable for the currently loaded page.
|
||
2896 | */
|
||
2897 | protected function drupalSetSettings($settings) { |
||
2898 | $this->drupalSettings = $settings; |
||
2899 | } |
||
2900 | |||
2901 | /**
|
||
2902 | * Pass if the internal browser's URL matches the given path.
|
||
2903 | *
|
||
2904 | * @param $path
|
||
2905 | * The expected system path.
|
||
2906 | * @param $options
|
||
2907 | * (optional) Any additional options to pass for $path to url().
|
||
2908 | * @param $message
|
||
2909 | * Message to display.
|
||
2910 | * @param $group
|
||
2911 | * The group this message belongs to, defaults to 'Other'.
|
||
2912 | *
|
||
2913 | * @return
|
||
2914 | * TRUE on pass, FALSE on fail.
|
||
2915 | */
|
||
2916 | protected function assertUrl($path, array $options = array(), $message = '', $group = 'Other') { |
||
2917 | if (!$message) { |
||
2918 | $message = t('Current URL is @url.', array( |
||
2919 | '@url' => var_export(url($path, $options), TRUE), |
||
2920 | )); |
||
2921 | } |
||
2922 | $options['absolute'] = TRUE; |
||
2923 | return $this->assertEqual($this->getUrl(), url($path, $options), $message, $group); |
||
2924 | } |
||
2925 | |||
2926 | /**
|
||
2927 | * Pass if the raw text IS found on the loaded page, fail otherwise. Raw text
|
||
2928 | * refers to the raw HTML that the page generated.
|
||
2929 | *
|
||
2930 | * @param $raw
|
||
2931 | * Raw (HTML) string to look for.
|
||
2932 | * @param $message
|
||
2933 | * Message to display.
|
||
2934 | * @param $group
|
||
2935 | * The group this message belongs to, defaults to 'Other'.
|
||
2936 | * @return
|
||
2937 | * TRUE on pass, FALSE on fail.
|
||
2938 | */
|
||
2939 | protected function assertRaw($raw, $message = '', $group = 'Other') { |
||
2940 | if (!$message) { |
||
2941 | $message = t('Raw "@raw" found', array('@raw' => $raw)); |
||
2942 | } |
||
2943 | return $this->assert(strpos($this->drupalGetContent(), $raw) !== FALSE, $message, $group); |
||
2944 | } |
||
2945 | |||
2946 | /**
|
||
2947 | * Pass if the raw text is NOT found on the loaded page, fail otherwise. Raw text
|
||
2948 | * refers to the raw HTML that the page generated.
|
||
2949 | *
|
||
2950 | * @param $raw
|
||
2951 | * Raw (HTML) string to look for.
|
||
2952 | * @param $message
|
||
2953 | * Message to display.
|
||
2954 | * @param $group
|
||
2955 | * The group this message belongs to, defaults to 'Other'.
|
||
2956 | * @return
|
||
2957 | * TRUE on pass, FALSE on fail.
|
||
2958 | */
|
||
2959 | protected function assertNoRaw($raw, $message = '', $group = 'Other') { |
||
2960 | if (!$message) { |
||
2961 | $message = t('Raw "@raw" not found', array('@raw' => $raw)); |
||
2962 | } |
||
2963 | return $this->assert(strpos($this->drupalGetContent(), $raw) === FALSE, $message, $group); |
||
2964 | } |
||
2965 | |||
2966 | /**
|
||
2967 | * Pass if the text IS found on the text version of the page. The text version
|
||
2968 | * is the equivalent of what a user would see when viewing through a web browser.
|
||
2969 | * In other words the HTML has been filtered out of the contents.
|
||
2970 | *
|
||
2971 | * @param $text
|
||
2972 | * Plain text to look for.
|
||
2973 | * @param $message
|
||
2974 | * Message to display.
|
||
2975 | * @param $group
|
||
2976 | * The group this message belongs to, defaults to 'Other'.
|
||
2977 | * @return
|
||
2978 | * TRUE on pass, FALSE on fail.
|
||
2979 | */
|
||
2980 | protected function assertText($text, $message = '', $group = 'Other') { |
||
2981 | return $this->assertTextHelper($text, $message, $group, FALSE); |
||
2982 | } |
||
2983 | |||
2984 | /**
|
||
2985 | * Pass if the text is NOT found on the text version of the page. The text version
|
||
2986 | * is the equivalent of what a user would see when viewing through a web browser.
|
||
2987 | * In other words the HTML has been filtered out of the contents.
|
||
2988 | *
|
||
2989 | * @param $text
|
||
2990 | * Plain text to look for.
|
||
2991 | * @param $message
|
||
2992 | * Message to display.
|
||
2993 | * @param $group
|
||
2994 | * The group this message belongs to, defaults to 'Other'.
|
||
2995 | * @return
|
||
2996 | * TRUE on pass, FALSE on fail.
|
||
2997 | */
|
||
2998 | protected function assertNoText($text, $message = '', $group = 'Other') { |
||
2999 | return $this->assertTextHelper($text, $message, $group, TRUE); |
||
3000 | } |
||
3001 | |||
3002 | /**
|
||
3003 | * Helper for assertText and assertNoText.
|
||
3004 | *
|
||
3005 | * It is not recommended to call this function directly.
|
||
3006 | *
|
||
3007 | * @param $text
|
||
3008 | * Plain text to look for.
|
||
3009 | * @param $message
|
||
3010 | * Message to display.
|
||
3011 | * @param $group
|
||
3012 | * The group this message belongs to.
|
||
3013 | * @param $not_exists
|
||
3014 | * TRUE if this text should not exist, FALSE if it should.
|
||
3015 | * @return
|
||
3016 | * TRUE on pass, FALSE on fail.
|
||
3017 | */
|
||
3018 | protected function assertTextHelper($text, $message = '', $group, $not_exists) { |
||
3019 | if ($this->plainTextContent === FALSE) { |
||
3020 | $this->plainTextContent = filter_xss($this->drupalGetContent(), array()); |
||
3021 | } |
||
3022 | if (!$message) { |
||
3023 | $message = !$not_exists ? t('"@text" found', array('@text' => $text)) : t('"@text" not found', array('@text' => $text)); |
||
3024 | } |
||
3025 | return $this->assert($not_exists == (strpos($this->plainTextContent, $text) === FALSE), $message, $group); |
||
3026 | } |
||
3027 | |||
3028 | /**
|
||
3029 | * Pass if the text is found ONLY ONCE on the text version of the page.
|
||
3030 | *
|
||
3031 | * The text version is the equivalent of what a user would see when viewing
|
||
3032 | * through a web browser. In other words the HTML has been filtered out of
|
||
3033 | * the contents.
|
||
3034 | *
|
||
3035 | * @param $text
|
||
3036 | * Plain text to look for.
|
||
3037 | * @param $message
|
||
3038 | * Message to display.
|
||
3039 | * @param $group
|
||
3040 | * The group this message belongs to, defaults to 'Other'.
|
||
3041 | * @return
|
||
3042 | * TRUE on pass, FALSE on fail.
|
||
3043 | */
|
||
3044 | protected function assertUniqueText($text, $message = '', $group = 'Other') { |
||
3045 | return $this->assertUniqueTextHelper($text, $message, $group, TRUE); |
||
3046 | } |
||
3047 | |||
3048 | /**
|
||
3049 | * Pass if the text is found MORE THAN ONCE on the text version of the page.
|
||
3050 | *
|
||
3051 | * The text version is the equivalent of what a user would see when viewing
|
||
3052 | * through a web browser. In other words the HTML has been filtered out of
|
||
3053 | * the contents.
|
||
3054 | *
|
||
3055 | * @param $text
|
||
3056 | * Plain text to look for.
|
||
3057 | * @param $message
|
||
3058 | * Message to display.
|
||
3059 | * @param $group
|
||
3060 | * The group this message belongs to, defaults to 'Other'.
|
||
3061 | * @return
|
||
3062 | * TRUE on pass, FALSE on fail.
|
||
3063 | */
|
||
3064 | protected function assertNoUniqueText($text, $message = '', $group = 'Other') { |
||
3065 | return $this->assertUniqueTextHelper($text, $message, $group, FALSE); |
||
3066 | } |
||
3067 | |||
3068 | /**
|
||
3069 | * Helper for assertUniqueText and assertNoUniqueText.
|
||
3070 | *
|
||
3071 | * It is not recommended to call this function directly.
|
||
3072 | *
|
||
3073 | * @param $text
|
||
3074 | * Plain text to look for.
|
||
3075 | * @param $message
|
||
3076 | * Message to display.
|
||
3077 | * @param $group
|
||
3078 | * The group this message belongs to.
|
||
3079 | * @param $be_unique
|
||
3080 | * TRUE if this text should be found only once, FALSE if it should be found more than once.
|
||
3081 | * @return
|
||
3082 | * TRUE on pass, FALSE on fail.
|
||
3083 | */
|
||
3084 | protected function assertUniqueTextHelper($text, $message = '', $group, $be_unique) { |
||
3085 | if ($this->plainTextContent === FALSE) { |
||
3086 | $this->plainTextContent = filter_xss($this->drupalGetContent(), array()); |
||
3087 | } |
||
3088 | if (!$message) { |
||
3089 | $message = '"' . $text . '"' . ($be_unique ? ' found only once' : ' found more than once'); |
||
3090 | } |
||
3091 | $first_occurance = strpos($this->plainTextContent, $text); |
||
3092 | if ($first_occurance === FALSE) { |
||
3093 | return $this->assert(FALSE, $message, $group); |
||
3094 | } |
||
3095 | $offset = $first_occurance + strlen($text); |
||
3096 | $second_occurance = strpos($this->plainTextContent, $text, $offset); |
||
3097 | return $this->assert($be_unique == ($second_occurance === FALSE), $message, $group); |
||
3098 | } |
||
3099 | |||
3100 | /**
|
||
3101 | * Will trigger a pass if the Perl regex pattern is found in the raw content.
|
||
3102 | *
|
||
3103 | * @param $pattern
|
||
3104 | * Perl regex to look for including the regex delimiters.
|
||
3105 | * @param $message
|
||
3106 | * Message to display.
|
||
3107 | * @param $group
|
||
3108 | * The group this message belongs to.
|
||
3109 | * @return
|
||
3110 | * TRUE on pass, FALSE on fail.
|
||
3111 | */
|
||
3112 | protected function assertPattern($pattern, $message = '', $group = 'Other') { |
||
3113 | if (!$message) { |
||
3114 | $message = t('Pattern "@pattern" found', array('@pattern' => $pattern)); |
||
3115 | } |
||
3116 | return $this->assert((bool) preg_match($pattern, $this->drupalGetContent()), $message, $group); |
||
3117 | } |
||
3118 | |||
3119 | /**
|
||
3120 | * Will trigger a pass if the perl regex pattern is not present in raw content.
|
||
3121 | *
|
||
3122 | * @param $pattern
|
||
3123 | * Perl regex to look for including the regex delimiters.
|
||
3124 | * @param $message
|
||
3125 | * Message to display.
|
||
3126 | * @param $group
|
||
3127 | * The group this message belongs to.
|
||
3128 | * @return
|
||
3129 | * TRUE on pass, FALSE on fail.
|
||
3130 | */
|
||
3131 | protected function assertNoPattern($pattern, $message = '', $group = 'Other') { |
||
3132 | if (!$message) { |
||
3133 | $message = t('Pattern "@pattern" not found', array('@pattern' => $pattern)); |
||
3134 | } |
||
3135 | return $this->assert(!preg_match($pattern, $this->drupalGetContent()), $message, $group); |
||
3136 | } |
||
3137 | |||
3138 | /**
|
||
3139 | * Pass if the page title is the given string.
|
||
3140 | *
|
||
3141 | * @param $title
|
||
3142 | * The string the title should be.
|
||
3143 | * @param $message
|
||
3144 | * Message to display.
|
||
3145 | * @param $group
|
||
3146 | * The group this message belongs to.
|
||
3147 | * @return
|
||
3148 | * TRUE on pass, FALSE on fail.
|
||
3149 | */
|
||
3150 | protected function assertTitle($title, $message = '', $group = 'Other') { |
||
3151 | $actual = (string) current($this->xpath('//title')); |
||
3152 | if (!$message) { |
||
3153 | $message = t('Page title @actual is equal to @expected.', array( |
||
3154 | '@actual' => var_export($actual, TRUE), |
||
3155 | '@expected' => var_export($title, TRUE), |
||
3156 | )); |
||
3157 | } |
||
3158 | return $this->assertEqual($actual, $title, $message, $group); |
||
3159 | } |
||
3160 | |||
3161 | /**
|
||
3162 | * Pass if the page title is not the given string.
|
||
3163 | *
|
||
3164 | * @param $title
|
||
3165 | * The string the title should not be.
|
||
3166 | * @param $message
|
||
3167 | * Message to display.
|
||
3168 | * @param $group
|
||
3169 | * The group this message belongs to.
|
||
3170 | * @return
|
||
3171 | * TRUE on pass, FALSE on fail.
|
||
3172 | */
|
||
3173 | protected function assertNoTitle($title, $message = '', $group = 'Other') { |
||
3174 | $actual = (string) current($this->xpath('//title')); |
||
3175 | if (!$message) { |
||
3176 | $message = t('Page title @actual is not equal to @unexpected.', array( |
||
3177 | '@actual' => var_export($actual, TRUE), |
||
3178 | '@unexpected' => var_export($title, TRUE), |
||
3179 | )); |
||
3180 | } |
||
3181 | return $this->assertNotEqual($actual, $title, $message, $group); |
||
3182 | } |
||
3183 | |||
3184 | /**
|
||
3185 | * Asserts themed output.
|
||
3186 | *
|
||
3187 | * @param $callback
|
||
3188 | * The name of the theme function to invoke; e.g. 'links' for theme_links().
|
||
3189 | * @param $variables
|
||
3190 | e33d3026 | Julien Enselme | * (optional) An array of variables to pass to the theme function.
|
3191 | 85ad3d82 | Assos Assos | * @param $expected
|
3192 | * The expected themed output string.
|
||
3193 | * @param $message
|
||
3194 | * (optional) A message to display with the assertion. Do not translate
|
||
3195 | * messages: use format_string() to embed variables in the message text, not
|
||
3196 | * t(). If left blank, a default message will be displayed.
|
||
3197 | * @param $group
|
||
3198 | * (optional) The group this message is in, which is displayed in a column
|
||
3199 | * in test output. Use 'Debug' to indicate this is debugging output. Do not
|
||
3200 | * translate this string. Defaults to 'Other'; most tests do not override
|
||
3201 | * this default.
|
||
3202 | *
|
||
3203 | * @return
|
||
3204 | * TRUE on pass, FALSE on fail.
|
||
3205 | */
|
||
3206 | protected function assertThemeOutput($callback, array $variables = array(), $expected, $message = '', $group = 'Other') { |
||
3207 | $output = theme($callback, $variables); |
||
3208 | $this->verbose('Variables:' . '<pre>' . check_plain(var_export($variables, TRUE)) . '</pre>' |
||
3209 | . '<hr />' . 'Result:' . '<pre>' . check_plain(var_export($output, TRUE)) . '</pre>' |
||
3210 | . '<hr />' . 'Expected:' . '<pre>' . check_plain(var_export($expected, TRUE)) . '</pre>' |
||
3211 | . '<hr />' . $output |
||
3212 | ); |
||
3213 | if (!$message) { |
||
3214 | $message = '%callback rendered correctly.'; |
||
3215 | } |
||
3216 | $message = format_string($message, array('%callback' => 'theme_' . $callback . '()')); |
||
3217 | return $this->assertIdentical($output, $expected, $message, $group); |
||
3218 | } |
||
3219 | |||
3220 | /**
|
||
3221 | * Asserts that a field exists in the current page by the given XPath.
|
||
3222 | *
|
||
3223 | * @param $xpath
|
||
3224 | * XPath used to find the field.
|
||
3225 | * @param $value
|
||
3226 | e33d3026 | Julien Enselme | * (optional) Value of the field to assert. You may pass in NULL (default)
|
3227 | * to skip checking the actual value, while still checking that the field
|
||
3228 | * exists.
|
||
3229 | 85ad3d82 | Assos Assos | * @param $message
|
3230 | * (optional) Message to display.
|
||
3231 | * @param $group
|
||
3232 | * (optional) The group this message belongs to.
|
||
3233 | *
|
||
3234 | * @return
|
||
3235 | * TRUE on pass, FALSE on fail.
|
||
3236 | */
|
||
3237 | protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $group = 'Other') { |
||
3238 | $fields = $this->xpath($xpath); |
||
3239 | |||
3240 | // If value specified then check array for match.
|
||
3241 | $found = TRUE; |
||
3242 | if (isset($value)) { |
||
3243 | $found = FALSE; |
||
3244 | if ($fields) { |
||
3245 | foreach ($fields as $field) { |
||
3246 | if (isset($field['value']) && $field['value'] == $value) { |
||
3247 | // Input element with correct value.
|
||
3248 | $found = TRUE; |
||
3249 | } |
||
3250 | elseif (isset($field->option)) { |
||
3251 | // Select element found.
|
||
3252 | if ($this->getSelectedItem($field) == $value) { |
||
3253 | $found = TRUE; |
||
3254 | } |
||
3255 | else {
|
||
3256 | // No item selected so use first item.
|
||
3257 | $items = $this->getAllOptions($field); |
||
3258 | if (!empty($items) && $items[0]['value'] == $value) { |
||
3259 | $found = TRUE; |
||
3260 | } |
||
3261 | } |
||
3262 | } |
||
3263 | elseif ((string) $field == $value) { |
||
3264 | // Text area with correct text.
|
||
3265 | $found = TRUE; |
||
3266 | } |
||
3267 | } |
||
3268 | } |
||
3269 | } |
||
3270 | return $this->assertTrue($fields && $found, $message, $group); |
||
3271 | } |
||
3272 | |||
3273 | /**
|
||
3274 | * Get the selected value from a select field.
|
||
3275 | *
|
||
3276 | * @param $element
|
||
3277 | * SimpleXMLElement select element.
|
||
3278 | * @return
|
||
3279 | * The selected value or FALSE.
|
||
3280 | */
|
||
3281 | protected function getSelectedItem(SimpleXMLElement $element) { |
||
3282 | foreach ($element->children() as $item) { |
||
3283 | if (isset($item['selected'])) { |
||
3284 | return $item['value']; |
||
3285 | } |
||
3286 | elseif ($item->getName() == 'optgroup') { |
||
3287 | if ($value = $this->getSelectedItem($item)) { |
||
3288 | return $value; |
||
3289 | } |
||
3290 | } |
||
3291 | } |
||
3292 | return FALSE; |
||
3293 | } |
||
3294 | |||
3295 | /**
|
||
3296 | e33d3026 | Julien Enselme | * Asserts that a field doesn't exist or its value doesn't match, by XPath.
|
3297 | 85ad3d82 | Assos Assos | *
|
3298 | * @param $xpath
|
||
3299 | * XPath used to find the field.
|
||
3300 | * @param $value
|
||
3301 | e33d3026 | Julien Enselme | * (optional) Value for the field, to assert that the field's value on the
|
3302 | * page doesn't match it. You may pass in NULL to skip checking the
|
||
3303 | * value, while still checking that the field doesn't exist.
|
||
3304 | 85ad3d82 | Assos Assos | * @param $message
|
3305 | * (optional) Message to display.
|
||
3306 | * @param $group
|
||
3307 | * (optional) The group this message belongs to.
|
||
3308 | *
|
||
3309 | * @return
|
||
3310 | * TRUE on pass, FALSE on fail.
|
||
3311 | */
|
||
3312 | protected function assertNoFieldByXPath($xpath, $value = NULL, $message = '', $group = 'Other') { |
||
3313 | $fields = $this->xpath($xpath); |
||
3314 | |||
3315 | // If value specified then check array for match.
|
||
3316 | $found = TRUE; |
||
3317 | if (isset($value)) { |
||
3318 | $found = FALSE; |
||
3319 | if ($fields) { |
||
3320 | foreach ($fields as $field) { |
||
3321 | if ($field['value'] == $value) { |
||
3322 | $found = TRUE; |
||
3323 | } |
||
3324 | } |
||
3325 | } |
||
3326 | } |
||
3327 | return $this->assertFalse($fields && $found, $message, $group); |
||
3328 | } |
||
3329 | |||
3330 | /**
|
||
3331 | * Asserts that a field exists in the current page with the given name and value.
|
||
3332 | *
|
||
3333 | * @param $name
|
||
3334 | * Name of field to assert.
|
||
3335 | * @param $value
|
||
3336 | e33d3026 | Julien Enselme | * (optional) Value of the field to assert. You may pass in NULL (default)
|
3337 | * to skip checking the actual value, while still checking that the field
|
||
3338 | * exists.
|
||
3339 | 85ad3d82 | Assos Assos | * @param $message
|
3340 | * Message to display.
|
||
3341 | * @param $group
|
||
3342 | * The group this message belongs to.
|
||
3343 | * @return
|
||
3344 | * TRUE on pass, FALSE on fail.
|
||
3345 | */
|
||
3346 | protected function assertFieldByName($name, $value = NULL, $message = NULL) { |
||
3347 | if (!isset($message)) { |
||
3348 | if (!isset($value)) { |
||
3349 | $message = t('Found field with name @name', array( |
||
3350 | '@name' => var_export($name, TRUE), |
||
3351 | )); |
||
3352 | } |
||
3353 | else {
|
||
3354 | $message = t('Found field with name @name and value @value', array( |
||
3355 | '@name' => var_export($name, TRUE), |
||
3356 | '@value' => var_export($value, TRUE), |
||
3357 | )); |
||
3358 | } |
||
3359 | } |
||
3360 | return $this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value, $message, t('Browser')); |
||
3361 | } |
||
3362 | |||
3363 | /**
|
||
3364 | * Asserts that a field does not exist with the given name and value.
|
||
3365 | *
|
||
3366 | * @param $name
|
||
3367 | * Name of field to assert.
|
||
3368 | * @param $value
|
||
3369 | e33d3026 | Julien Enselme | * (optional) Value for the field, to assert that the field's value on the
|
3370 | * page doesn't match it. You may pass in NULL to skip checking the
|
||
3371 | * value, while still checking that the field doesn't exist. However, the
|
||
3372 | * default value ('') asserts that the field value is not an empty string.
|
||
3373 | 85ad3d82 | Assos Assos | * @param $message
|
3374 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3375 | 85ad3d82 | Assos Assos | * @param $group
|
3376 | * The group this message belongs to.
|
||
3377 | * @return
|
||
3378 | * TRUE on pass, FALSE on fail.
|
||
3379 | */
|
||
3380 | protected function assertNoFieldByName($name, $value = '', $message = '') { |
||
3381 | return $this->assertNoFieldByXPath($this->constructFieldXpath('name', $name), $value, $message ? $message : t('Did not find field by name @name', array('@name' => $name)), t('Browser')); |
||
3382 | } |
||
3383 | |||
3384 | /**
|
||
3385 | e33d3026 | Julien Enselme | * Asserts that a field exists in the current page with the given ID and value.
|
3386 | 85ad3d82 | Assos Assos | *
|
3387 | * @param $id
|
||
3388 | e33d3026 | Julien Enselme | * ID of field to assert.
|
3389 | 85ad3d82 | Assos Assos | * @param $value
|
3390 | e33d3026 | Julien Enselme | * (optional) Value for the field to assert. You may pass in NULL to skip
|
3391 | * checking the value, while still checking that the field exists.
|
||
3392 | * However, the default value ('') asserts that the field value is an empty
|
||
3393 | * string.
|
||
3394 | 85ad3d82 | Assos Assos | * @param $message
|
3395 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3396 | 85ad3d82 | Assos Assos | * @param $group
|
3397 | * The group this message belongs to.
|
||
3398 | * @return
|
||
3399 | * TRUE on pass, FALSE on fail.
|
||
3400 | */
|
||
3401 | protected function assertFieldById($id, $value = '', $message = '') { |
||
3402 | return $this->assertFieldByXPath($this->constructFieldXpath('id', $id), $value, $message ? $message : t('Found field by id @id', array('@id' => $id)), t('Browser')); |
||
3403 | } |
||
3404 | |||
3405 | /**
|
||
3406 | e33d3026 | Julien Enselme | * Asserts that a field does not exist with the given ID and value.
|
3407 | 85ad3d82 | Assos Assos | *
|
3408 | * @param $id
|
||
3409 | e33d3026 | Julien Enselme | * ID of field to assert.
|
3410 | 85ad3d82 | Assos Assos | * @param $value
|
3411 | e33d3026 | Julien Enselme | * (optional) Value for the field, to assert that the field's value on the
|
3412 | * page doesn't match it. You may pass in NULL to skip checking the value,
|
||
3413 | * while still checking that the field doesn't exist. However, the default
|
||
3414 | * value ('') asserts that the field value is not an empty string.
|
||
3415 | 85ad3d82 | Assos Assos | * @param $message
|
3416 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3417 | 85ad3d82 | Assos Assos | * @param $group
|
3418 | * The group this message belongs to.
|
||
3419 | * @return
|
||
3420 | * TRUE on pass, FALSE on fail.
|
||
3421 | */
|
||
3422 | protected function assertNoFieldById($id, $value = '', $message = '') { |
||
3423 | return $this->assertNoFieldByXPath($this->constructFieldXpath('id', $id), $value, $message ? $message : t('Did not find field by id @id', array('@id' => $id)), t('Browser')); |
||
3424 | } |
||
3425 | |||
3426 | /**
|
||
3427 | * Asserts that a checkbox field in the current page is checked.
|
||
3428 | *
|
||
3429 | * @param $id
|
||
3430 | e33d3026 | Julien Enselme | * ID of field to assert.
|
3431 | 85ad3d82 | Assos Assos | * @param $message
|
3432 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3433 | 85ad3d82 | Assos Assos | * @return
|
3434 | * TRUE on pass, FALSE on fail.
|
||
3435 | */
|
||
3436 | protected function assertFieldChecked($id, $message = '') { |
||
3437 | $elements = $this->xpath('//input[@id=:id]', array(':id' => $id)); |
||
3438 | return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is checked.', array('@id' => $id)), t('Browser')); |
||
3439 | } |
||
3440 | |||
3441 | /**
|
||
3442 | * Asserts that a checkbox field in the current page is not checked.
|
||
3443 | *
|
||
3444 | * @param $id
|
||
3445 | e33d3026 | Julien Enselme | * ID of field to assert.
|
3446 | 85ad3d82 | Assos Assos | * @param $message
|
3447 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3448 | 85ad3d82 | Assos Assos | * @return
|
3449 | * TRUE on pass, FALSE on fail.
|
||
3450 | */
|
||
3451 | protected function assertNoFieldChecked($id, $message = '') { |
||
3452 | $elements = $this->xpath('//input[@id=:id]', array(':id' => $id)); |
||
3453 | return $this->assertTrue(isset($elements[0]) && empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is not checked.', array('@id' => $id)), t('Browser')); |
||
3454 | } |
||
3455 | |||
3456 | /**
|
||
3457 | * Asserts that a select option in the current page is checked.
|
||
3458 | *
|
||
3459 | * @param $id
|
||
3460 | e33d3026 | Julien Enselme | * ID of select field to assert.
|
3461 | 85ad3d82 | Assos Assos | * @param $option
|
3462 | * Option to assert.
|
||
3463 | * @param $message
|
||
3464 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3465 | 85ad3d82 | Assos Assos | * @return
|
3466 | * TRUE on pass, FALSE on fail.
|
||
3467 | *
|
||
3468 | * @todo $id is unusable. Replace with $name.
|
||
3469 | */
|
||
3470 | protected function assertOptionSelected($id, $option, $message = '') { |
||
3471 | $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option)); |
||
3472 | return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['selected']), $message ? $message : t('Option @option for field @id is selected.', array('@option' => $option, '@id' => $id)), t('Browser')); |
||
3473 | } |
||
3474 | |||
3475 | /**
|
||
3476 | * Asserts that a select option in the current page is not checked.
|
||
3477 | *
|
||
3478 | * @param $id
|
||
3479 | e33d3026 | Julien Enselme | * ID of select field to assert.
|
3480 | 85ad3d82 | Assos Assos | * @param $option
|
3481 | * Option to assert.
|
||
3482 | * @param $message
|
||
3483 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3484 | 85ad3d82 | Assos Assos | * @return
|
3485 | * TRUE on pass, FALSE on fail.
|
||
3486 | */
|
||
3487 | protected function assertNoOptionSelected($id, $option, $message = '') { |
||
3488 | $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option)); |
||
3489 | return $this->assertTrue(isset($elements[0]) && empty($elements[0]['selected']), $message ? $message : t('Option @option for field @id is not selected.', array('@option' => $option, '@id' => $id)), t('Browser')); |
||
3490 | } |
||
3491 | |||
3492 | /**
|
||
3493 | e33d3026 | Julien Enselme | * Asserts that a field exists with the given name or ID.
|
3494 | 85ad3d82 | Assos Assos | *
|
3495 | * @param $field
|
||
3496 | e33d3026 | Julien Enselme | * Name or ID of field to assert.
|
3497 | 85ad3d82 | Assos Assos | * @param $message
|
3498 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3499 | 85ad3d82 | Assos Assos | * @param $group
|
3500 | * The group this message belongs to.
|
||
3501 | * @return
|
||
3502 | * TRUE on pass, FALSE on fail.
|
||
3503 | */
|
||
3504 | protected function assertField($field, $message = '', $group = 'Other') { |
||
3505 | return $this->assertFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field), NULL, $message, $group); |
||
3506 | } |
||
3507 | |||
3508 | /**
|
||
3509 | e33d3026 | Julien Enselme | * Asserts that a field does not exist with the given name or ID.
|
3510 | 85ad3d82 | Assos Assos | *
|
3511 | * @param $field
|
||
3512 | e33d3026 | Julien Enselme | * Name or ID of field to assert.
|
3513 | 85ad3d82 | Assos Assos | * @param $message
|
3514 | e33d3026 | Julien Enselme | * (optional) Message to display.
|
3515 | 85ad3d82 | Assos Assos | * @param $group
|
3516 | * The group this message belongs to.
|
||
3517 | * @return
|
||
3518 | * TRUE on pass, FALSE on fail.
|
||
3519 | */
|
||
3520 | protected function assertNoField($field, $message = '', $group = 'Other') { |
||
3521 | return $this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field), NULL, $message, $group); |
||
3522 | } |
||
3523 | |||
3524 | /**
|
||
3525 | * Asserts that each HTML ID is used for just a single element.
|
||
3526 | *
|
||
3527 | * @param $message
|
||
3528 | * Message to display.
|
||
3529 | * @param $group
|
||
3530 | * The group this message belongs to.
|
||
3531 | * @param $ids_to_skip
|
||
3532 | * An optional array of ids to skip when checking for duplicates. It is
|
||
3533 | * always a bug to have duplicate HTML IDs, so this parameter is to enable
|
||
3534 | * incremental fixing of core code. Whenever a test passes this parameter,
|
||
3535 | * it should add a "todo" comment above the call to this function explaining
|
||
3536 | * the legacy bug that the test wishes to ignore and including a link to an
|
||
3537 | * issue that is working to fix that legacy bug.
|
||
3538 | * @return
|
||
3539 | * TRUE on pass, FALSE on fail.
|
||
3540 | */
|
||
3541 | protected function assertNoDuplicateIds($message = '', $group = 'Other', $ids_to_skip = array()) { |
||
3542 | $status = TRUE; |
||
3543 | foreach ($this->xpath('//*[@id]') as $element) { |
||
3544 | $id = (string) $element['id']; |
||
3545 | if (isset($seen_ids[$id]) && !in_array($id, $ids_to_skip)) { |
||
3546 | $this->fail(t('The HTML ID %id is unique.', array('%id' => $id)), $group); |
||
3547 | $status = FALSE; |
||
3548 | } |
||
3549 | $seen_ids[$id] = TRUE; |
||
3550 | } |
||
3551 | return $this->assert($status, $message, $group); |
||
3552 | } |
||
3553 | |||
3554 | /**
|
||
3555 | * Helper function: construct an XPath for the given set of attributes and value.
|
||
3556 | *
|
||
3557 | * @param $attribute
|
||
3558 | * Field attributes.
|
||
3559 | * @param $value
|
||
3560 | * Value of field.
|
||
3561 | * @return
|
||
3562 | * XPath for specified values.
|
||
3563 | */
|
||
3564 | protected function constructFieldXpath($attribute, $value) { |
||
3565 | $xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]'; |
||
3566 | return $this->buildXPathQuery($xpath, array(':value' => $value)); |
||
3567 | } |
||
3568 | |||
3569 | /**
|
||
3570 | * Asserts the page responds with the specified response code.
|
||
3571 | *
|
||
3572 | * @param $code
|
||
3573 | * Response code. For example 200 is a successful page request. For a list
|
||
3574 | * of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
|
||
3575 | * @param $message
|
||
3576 | * Message to display.
|
||
3577 | * @return
|
||
3578 | * Assertion result.
|
||
3579 | */
|
||
3580 | protected function assertResponse($code, $message = '') { |
||
3581 | $curl_code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); |
||
3582 | $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code; |
||
3583 | return $this->assertTrue($match, $message ? $message : t('HTTP response expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)), t('Browser')); |
||
3584 | } |
||
3585 | |||
3586 | /**
|
||
3587 | * Asserts the page did not return the specified response code.
|
||
3588 | *
|
||
3589 | * @param $code
|
||
3590 | * Response code. For example 200 is a successful page request. For a list
|
||
3591 | * of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
|
||
3592 | * @param $message
|
||
3593 | * Message to display.
|
||
3594 | *
|
||
3595 | * @return
|
||
3596 | * Assertion result.
|
||
3597 | */
|
||
3598 | protected function assertNoResponse($code, $message = '') { |
||
3599 | $curl_code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); |
||
3600 | $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code; |
||
3601 | return $this->assertFalse($match, $message ? $message : t('HTTP response not expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)), t('Browser')); |
||
3602 | } |
||
3603 | |||
3604 | /**
|
||
3605 | * Asserts that the most recently sent e-mail message has the given value.
|
||
3606 | *
|
||
3607 | * The field in $name must have the content described in $value.
|
||
3608 | *
|
||
3609 | * @param $name
|
||
3610 | * Name of field or message property to assert. Examples: subject, body, id, ...
|
||
3611 | * @param $value
|
||
3612 | * Value of the field to assert.
|
||
3613 | * @param $message
|
||
3614 | * Message to display.
|
||
3615 | *
|
||
3616 | * @return
|
||
3617 | * TRUE on pass, FALSE on fail.
|
||
3618 | */
|
||
3619 | protected function assertMail($name, $value = '', $message = '') { |
||
3620 | $captured_emails = variable_get('drupal_test_email_collector', array()); |
||
3621 | $email = end($captured_emails); |
||
3622 | return $this->assertTrue($email && isset($email[$name]) && $email[$name] == $value, $message, t('E-mail')); |
||
3623 | } |
||
3624 | |||
3625 | /**
|
||
3626 | * Asserts that the most recently sent e-mail message has the string in it.
|
||
3627 | *
|
||
3628 | * @param $field_name
|
||
3629 | * Name of field or message property to assert: subject, body, id, ...
|
||
3630 | * @param $string
|
||
3631 | * String to search for.
|
||
3632 | * @param $email_depth
|
||
3633 | * Number of emails to search for string, starting with most recent.
|
||
3634 | *
|
||
3635 | * @return
|
||
3636 | * TRUE on pass, FALSE on fail.
|
||
3637 | */
|
||
3638 | protected function assertMailString($field_name, $string, $email_depth) { |
||
3639 | $mails = $this->drupalGetMails(); |
||
3640 | $string_found = FALSE; |
||
3641 | for ($i = sizeof($mails) -1; $i >= sizeof($mails) - $email_depth && $i >= 0; $i--) { |
||
3642 | $mail = $mails[$i]; |
||
3643 | // Normalize whitespace, as we don't know what the mail system might have
|
||
3644 | // done. Any run of whitespace becomes a single space.
|
||
3645 | $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]); |
||
3646 | $normalized_string = preg_replace('/\s+/', ' ', $string); |
||
3647 | $string_found = (FALSE !== strpos($normalized_mail, $normalized_string)); |
||
3648 | if ($string_found) { |
||
3649 | break;
|
||
3650 | } |
||
3651 | } |
||
3652 | return $this->assertTrue($string_found, t('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $string))); |
||
3653 | } |
||
3654 | |||
3655 | /**
|
||
3656 | * Asserts that the most recently sent e-mail message has the pattern in it.
|
||
3657 | *
|
||
3658 | * @param $field_name
|
||
3659 | * Name of field or message property to assert: subject, body, id, ...
|
||
3660 | * @param $regex
|
||
3661 | * Pattern to search for.
|
||
3662 | *
|
||
3663 | * @return
|
||
3664 | * TRUE on pass, FALSE on fail.
|
||
3665 | */
|
||
3666 | protected function assertMailPattern($field_name, $regex, $message) { |
||
3667 | $mails = $this->drupalGetMails(); |
||
3668 | $mail = end($mails); |
||
3669 | $regex_found = preg_match("/$regex/", $mail[$field_name]); |
||
3670 | return $this->assertTrue($regex_found, t('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $regex))); |
||
3671 | } |
||
3672 | |||
3673 | /**
|
||
3674 | * Outputs to verbose the most recent $count emails sent.
|
||
3675 | *
|
||
3676 | * @param $count
|
||
3677 | * Optional number of emails to output.
|
||
3678 | */
|
||
3679 | protected function verboseEmail($count = 1) { |
||
3680 | $mails = $this->drupalGetMails(); |
||
3681 | for ($i = sizeof($mails) -1; $i >= sizeof($mails) - $count && $i >= 0; $i--) { |
||
3682 | $mail = $mails[$i]; |
||
3683 | $this->verbose(t('Email:') . '<pre>' . print_r($mail, TRUE) . '</pre>'); |
||
3684 | } |
||
3685 | } |
||
3686 | } |
||
3687 | |||
3688 | /**
|
||
3689 | * Logs verbose message in a text file.
|
||
3690 | *
|
||
3691 | * If verbose mode is enabled then page requests will be dumped to a file and
|
||
3692 | * presented on the test result screen. The messages will be placed in a file
|
||
3693 | * located in the simpletest directory in the original file system.
|
||
3694 | *
|
||
3695 | * @param $message
|
||
3696 | * The verbose message to be stored.
|
||
3697 | * @param $original_file_directory
|
||
3698 | * The original file directory, before it was changed for testing purposes.
|
||
3699 | * @param $test_class
|
||
3700 | * The active test case class.
|
||
3701 | *
|
||
3702 | * @return
|
||
3703 | * The ID of the message to be placed in related assertion messages.
|
||
3704 | *
|
||
3705 | * @see DrupalTestCase->originalFileDirectory
|
||
3706 | * @see DrupalWebTestCase->verbose()
|
||
3707 | */
|
||
3708 | function simpletest_verbose($message, $original_file_directory = NULL, $test_class = NULL) { |
||
3709 | static $file_directory = NULL, $class = NULL, $id = 1, $verbose = NULL; |
||
3710 | |||
3711 | // Will pass first time during setup phase, and when verbose is TRUE.
|
||
3712 | if (!isset($original_file_directory) && !$verbose) { |
||
3713 | return FALSE; |
||
3714 | } |
||
3715 | |||
3716 | if ($message && $file_directory) { |
||
3717 | $message = '<hr />ID #' . $id . ' (<a href="' . $class . '-' . ($id - 1) . '.html">Previous</a> | <a href="' . $class . '-' . ($id + 1) . '.html">Next</a>)<hr />' . $message; |
||
3718 | file_put_contents($file_directory . "/simpletest/verbose/$class-$id.html", $message, FILE_APPEND); |
||
3719 | return $id++; |
||
3720 | } |
||
3721 | |||
3722 | if ($original_file_directory) { |
||
3723 | $file_directory = $original_file_directory; |
||
3724 | $class = $test_class; |
||
3725 | $verbose = variable_get('simpletest_verbose', TRUE); |
||
3726 | $directory = $file_directory . '/simpletest/verbose'; |
||
3727 | $writable = file_prepare_directory($directory, FILE_CREATE_DIRECTORY); |
||
3728 | if ($writable && !file_exists($directory . '/.htaccess')) { |
||
3729 | file_put_contents($directory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n"); |
||
3730 | } |
||
3731 | return $writable; |
||
3732 | } |
||
3733 | return FALSE; |
||
3734 | } |