Line # | Frequency | Source Line |
1 | | <?php
|
2 | | // $Id: drupal_test_case.php,v 1.82 2008/04/04 14:19:33 rokZlender Exp $
|
3 | |
|
4 | | /**
|
5 | | * Test case for typical Drupal tests.
|
6 | | */
|
7 | | class DrupalTestCase extends UnitTestCase {
|
8 | | protected $_logged_in = FALSE;
|
9 | | protected $_content;
|
10 | | protected $plain_text;
|
11 | | protected $ch;
|
12 | | protected $_modules = array();
|
13 | | // We do not reuse the cookies in further runs, so we do not need a file
|
14 | | // but we still need cookie handling, so we set the jar to NULL
|
15 | | protected $cookie_file = NULL;
|
16 | | // Overwrite this any time to supply cURL options as necessary,
|
17 | | // DrupalTestCase itself never sets this but always obeys whats set.
|
18 | | protected $curl_options = array();
|
19 | |
|
20 | | /**
|
21 | | * Retrieve the test information from getInfo().
|
22 | | *
|
23 | | * @param string $label Name of the test to be used by the SimpleTest library.
|
24 | | */
|
25 | | function __construct($label = NULL) {
|
26 | 1 | if (!$label) {
|
27 | 1 | if (method_exists($this, 'getInfo')) {
|
28 | 1 | $info = $this->getInfo();
|
29 | 1 | $label = $info['name'];
|
30 | | }
|
31 | | }
|
32 | 1 | parent::__construct($label);
|
33 | | }
|
34 | |
|
35 | | /**
|
36 | | * Creates a node based on default settings.
|
37 | | *
|
38 | | * @param settings
|
39 | | * An assocative array of settings to change from the defaults, keys are
|
40 | | * node properties, for example 'body' => 'Hello, world!'.
|
41 | | * @return object Created node object.
|
42 | | */
|
43 | | function drupalCreateNode($settings = array()) {
|
44 | | // Populate defaults array
|
45 | | $defaults = array(
|
46 | | 'body' => $this->randomName(32),
|
47 | | 'title' => $this->randomName(8),
|
48 | | 'comment' => 2,
|
49 | | 'changed' => time(),
|
50 | | 'format' => FILTER_FORMAT_DEFAULT,
|
51 | | 'moderate' => 0,
|
52 | | 'promote' => 0,
|
53 | | 'revision' => 1,
|
54 | | 'log' => '',
|
55 | | 'status' => 1,
|
56 | | 'sticky' => 0,
|
57 | | 'type' => 'page',
|
58 | | 'revisions' => NULL,
|
59 | | 'taxonomy' => NULL,
|
60 | | );
|
61 | | $defaults['teaser'] = $defaults['body'];
|
62 | | // If we already have a node, we use the original node's created time, and this
|
63 | | if (isset($defaults['created'])) {
|
64 | | $defaults['date'] = format_date($defaults['created'], 'custom', 'Y-m-d H:i:s O');
|
65 | | }
|
66 | | if (empty($settings['uid'])) {
|
67 | | global $user;
|
68 | | $defaults['uid'] = $user->uid;
|
69 | | }
|
70 | | $node = ($settings + $defaults);
|
71 | | $node = (object)$node;
|
72 | |
|
73 | | node_save($node);
|
74 | |
|
75 | | // small hack to link revisions to our test user
|
76 | | db_query('UPDATE {node_revisions} SET uid = %d WHERE vid = %d', $node->uid, $node->vid);
|
77 | | return $node;
|
78 | | }
|
79 | |
|
80 | | /**
|
81 | | * Creates a custom content type based on default settings.
|
82 | | *
|
83 | | * @param settings
|
84 | | * An array of settings to change from the defaults.
|
85 | | * Example: 'type' => 'foo'.
|
86 | | * @return object Created content type.
|
87 | | */
|
88 | | function drupalCreateContentType($settings = array()) {
|
89 | | // find a non-existent random type name.
|
90 | | do {
|
91 | | $name = strtolower($this->randomName(3, 'type_'));
|
92 | | } while (node_get_types('type', $name));
|
93 | |
|
94 | | // Populate defaults array
|
95 | | $defaults = array(
|
96 | | 'type' => $name,
|
97 | | 'name' => $name,
|
98 | | 'description' => '',
|
99 | | 'help' => '',
|
100 | | 'min_word_count' => 0,
|
101 | | 'title_label' => 'Title',
|
102 | | 'body_label' => 'Body',
|
103 | | 'has_title' => 1,
|
104 | | 'has_body' => 1,
|
105 | | );
|
106 | | // imposed values for a custom type
|
107 | | $forced = array(
|
108 | | 'orig_type' => '',
|
109 | | 'old_type' => '',
|
110 | | 'module' => 'node',
|
111 | | 'custom' => 1,
|
112 | | 'modified' => 1,
|
113 | | 'locked' => 0,
|
114 | | );
|
115 | | $type = $forced + $settings + $defaults;
|
116 | | $type = (object)$type;
|
117 | |
|
118 | | node_type_save($type);
|
119 | | node_types_rebuild();
|
120 | |
|
121 | | return $type;
|
122 | | }
|
123 | |
|
124 | | /**
|
125 | | * Get a list files that can be used in tests.
|
126 | | *
|
127 | | * @param string $type File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'.
|
128 | | * @param integer $size File size in bytes to match. Please check the tests/files folder.
|
129 | | * @return array List of files that match filter.
|
130 | | */
|
131 | | function drupalGetTestFiles($type, $size = NULL) {
|
132 | | $files = array();
|
133 | |
|
134 | | // Make sure type is valid.
|
135 | | if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
|
136 | | $path = file_directory_path() .'/simpletest';
|
137 | | $files = file_scan_directory($path, $type .'\-.*');
|
138 | |
|
139 | | // If size is set then remove any files that are not of that size.
|
140 | | if ($size !== NULL) {
|
141 | | foreach ($files as $file) {
|
142 | | $stats = stat($file->filename);
|
143 | | if ($stats['size'] != $size) {
|
144 | | unset($files[$file->filename]);
|
145 | | }
|
146 | | }
|
147 | | }
|
148 | | }
|
149 | | return $files;
|
150 | | }
|
151 | |
|
152 | | /**
|
153 | | * Generates a random string.
|
154 | | *
|
155 | | * @param integer $number Number of characters in length to append to the prefix.
|
156 | | * @param string $prefix Prefix to use.
|
157 | | * @return string Randomly generated string.
|
158 | | */
|
159 | | function randomName($number = 4, $prefix = 'simpletest_') {
|
160 | 1 | $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
|
161 | 1 | for ($x = 0; $x < $number; $x++) {
|
162 | 1 | $prefix .= $chars{mt_rand(0, strlen($chars)-1)};
|
163 | 1 | if ($x == 0) {
|
164 | 1 | $chars .= '0123456789';
|
165 | | }
|
166 | | }
|
167 | 1 | return $prefix;
|
168 | | }
|
169 | |
|
170 | | /**
|
171 | | * Enables a drupal module in the test database. Any module that is not
|
172 | | * part of the required core modules needs to be enabled in order to use
|
173 | | * it in a test.
|
174 | | *
|
175 | | * @param string $name Name of the module to enable.
|
176 | | * @return boolean Success.
|
177 | | */
|
178 | | function drupalModuleEnable($name) {
|
179 | | if (module_exists($name)) {
|
180 | | $this->pass(" [module] $name already enabled");
|
181 | | return TRUE;
|
182 | | }
|
183 | | $this->checkOriginalModules();
|
184 | | if (array_search($name, $this->_modules) === FALSE) {
|
185 | | $this->_modules[$name] = $name;
|
186 | | $form_state['values'] = array('status' => $this->_modules, 'op' => t('Save configuration'));
|
187 | | drupal_execute('system_modules', $form_state);
|
188 | |
|
189 | | //rebuilding all caches
|
190 | | drupal_rebuild_theme_registry();
|
191 | | node_types_rebuild();
|
192 | | menu_rebuild();
|
193 | | cache_clear_all('schema', 'cache');
|
194 | | module_rebuild_cache();
|
195 | | }
|
196 | | }
|
197 | |
|
198 | | /**
|
199 | | * Disables a drupal module in the test database.
|
200 | | *
|
201 | | * @param string $name Name of the module.
|
202 | | * @return boolean Success.
|
203 | | * @see drupalModuleEnable()
|
204 | | */
|
205 | | function drupalModuleDisable($name) {
|
206 | | if (!module_exists($name)) {
|
207 | | $this->pass(" [module] $name already disabled");
|
208 | | return TRUE;
|
209 | | }
|
210 | | $this->checkOriginalModules();
|
211 | | if (($key = array_search($name, $this->_modules)) !== FALSE) {
|
212 | | unset($this->_modules[$key]);
|
213 | | $form_state['values'] = array('status' => $this->_modules, 'op' => t('Save configuration'));
|
214 | | drupal_execute('system_modules', $form_state);
|
215 | |
|
216 | | //rebuilding all caches
|
217 | | drupal_rebuild_theme_registry();
|
218 | | node_types_rebuild();
|
219 | | menu_rebuild();
|
220 | | cache_clear_all('schema', 'cache');
|
221 | | module_rebuild_cache();
|
222 | | }
|
223 | | }
|
224 | |
|
225 | | /**
|
226 | | * Retrieves and saves current modules list into $_originalModules and $_modules.
|
227 | | */
|
228 | | function checkOriginalModules() {
|
229 | | if (empty($this->_originalModules)) {
|
230 | | require_once ('./modules/system/system.admin.inc');
|
231 | | $form_state = array();
|
232 | | $form = drupal_retrieve_form('system_modules', $form_state);
|
233 | | $this->_originalModules = drupal_map_assoc($form['status']['#default_value']);
|
234 | | $this->_modules = $this->_originalModules;
|
235 | | }
|
236 | | }
|
237 | |
|
238 | | /**
|
239 | | * Set a drupal variable in the test environment. Any variable settings that deviate
|
240 | | * from the default need to be set in the test.
|
241 | | *
|
242 | | * @param string $name Name of the variable to set.
|
243 | | * @param mixed $value Value to set.
|
244 | | */
|
245 | | function drupalVariableSet($name, $value) {
|
246 | | /* NULL variables would anyways result in default because of isset */
|
247 | | $old_value = variable_get($name, NULL);
|
248 | | if ($value !== $old_value) {
|
249 | | variable_set($name, $value);
|
250 | | }
|
251 | | }
|
252 | |
|
253 | | /**
|
254 | | * Create a user with a given set of permissions. The permissions correspond to the
|
255 | | * names given on the privileges page.
|
256 | | *
|
257 | | * @param array $permissions Array of permission names to assign to user.
|
258 | | * @return A fully loaded user object with pass_raw property, or FALSE if account
|
259 | | * creation fails.
|
260 | | */
|
261 | | function drupalCreateUser($permissions = NULL) {
|
262 | | // Create a role with the given permission set.
|
263 | 1 | $rid = $this->_drupalCreateRole($permissions);
|
264 | 1 | if (!$rid) {
|
265 | | return FALSE;
|
266 | | }
|
267 | |
|
268 | | // Create a user assigned to that role.
|
269 | | $edit = array();
|
270 | 1 | $edit['name'] = $this->randomName();
|
271 | 1 | $edit['mail'] = $edit['name'] .'@example.com';
|
272 | | $edit['roles'] = array($rid => $rid);
|
273 | 1 | $edit['pass'] = user_password();
|
274 | 1 | $edit['status'] = 1;
|
275 | |
|
276 | 1 | $account = user_save('', $edit);
|
277 | |
|
278 | 1 | $this->assertTrue(!empty($account->uid), " [user] name: $edit[name] pass: $edit[pass] created");
|
279 | 1 | if (empty($account->uid)) {
|
280 | | return FALSE;
|
281 | | }
|
282 | |
|
283 | | // Add the raw password so that we can log in as this user.
|
284 | 1 | $account->pass_raw = $edit['pass'];
|
285 | 1 | return $account;
|
286 | | }
|
287 | |
|
288 | | /**
|
289 | | * Internal helper function; Create a role with specified permissions.
|
290 | | *
|
291 | | * @param array $permissions Array of permission names to assign to role.
|
292 | | * @return integer Role ID of newly created role, or FALSE if role creation failed.
|
293 | | */
|
294 | | private function _drupalCreateRole($permissions = NULL) {
|
295 | | // Generate string version of permissions list.
|
296 | 1 | if ($permissions === NULL) {
|
297 | | $permission_string = 'access comments, access content, post comments, post comments without approval';
|
298 | | } else {
|
299 | 1 | $permission_string = implode(', ', $permissions);
|
300 | | }
|
301 | |
|
302 | | // Create new role.
|
303 | 1 | $role_name = $this->randomName();
|
304 | 1 | db_query("INSERT INTO {role} (name) VALUES ('%s')", $role_name);
|
305 | 1 | $role = db_fetch_object(db_query("SELECT * FROM {role} WHERE name = '%s'", $role_name));
|
306 | 1 | $this->assertTrue($role, " [role] created name: $role_name, id: " . (isset($role->rid) ? $role->rid : t('-n/a-')));
|
307 | 1 | if ($role && !empty($role->rid)) {
|
308 | | // Assign permissions to role and mark it for clean-up.
|
309 | 1 | db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')", $role->rid, $permission_string);
|
310 | 1 | $this->assertTrue(db_affected_rows(), ' [role] created permissions: ' . $permission_string);
|
311 | 1 | return $role->rid;
|
312 | | }
|
313 | | else {
|
314 | | return FALSE;
|
315 | | }
|
316 | | }
|
317 | |
|
318 | | /**
|
319 | | * Logs in a user with the internal browser. If already logged in then logs the current
|
320 | | * user out before logging in the specified user. If no user is specified then a new
|
321 | | * user will be created and logged in.
|
322 | | *
|
323 | | * @param object $user User object representing the user to login.
|
324 | | * @return object User that was logged in. Useful if no user was passed in order
|
325 | | * to retreive the created user.
|
326 | | */
|
327 | | function drupalLogin($user = NULL) {
|
328 | 1 | if ($this->_logged_in) {
|
329 | 1 | $this->drupalLogout();
|
330 | | }
|
331 | |
|
332 | 1 | if (!isset($user)) {
|
333 | | $user = $this->_drupalCreateRole();
|
334 | | }
|
335 | |
|
336 | | $edit = array(
|
337 | 1 | 'name' => $user->name,
|
338 | | 'pass' => $user->pass_raw
|
339 | | );
|
340 | 1 | $this->drupalPost('user', $edit, t('Log in'));
|
341 | |
|
342 | 1 | $pass = $this->assertText($user->name, ' [login] found name: '. $user->name);
|
343 | 1 | $pass = $pass && $this->assertNoText(t('The username %name has been blocked.', array('%name' => $user->name)), ' [login] not blocked');
|
344 | 1 | $pass = $pass && $this->assertNoText(t('The name %name is a reserved username.', array('%name' => $user->name)), ' [login] not reserved');
|
345 | |
|
346 | 1 | $this->_logged_in = $pass;
|
347 | |
|
348 | 1 | return $user;
|
349 | | }
|
350 | |
|
351 | | /*
|
352 | | * Logs a user out of the internal browser, then check the login page to confirm logout.
|
353 | | */
|
354 | | function drupalLogout() {
|
355 | | // Make a request to the logout page.
|
356 | 1 | $this->drupalGet('logout');
|
357 | |
|
358 | | // Load the user page, the idea being if you were properly logged out you should be seeing a login screen.
|
359 | 1 | $this->drupalGet('user');
|
360 | 1 | $pass = $this->assertField('name', t('[logout] Username field found.'));
|
361 | 1 | $pass = $pass && $this->assertField('pass', t('[logout] Password field found.'));
|
362 | |
|
363 | 1 | $this->_logged_in = !$pass;
|
364 | | }
|
365 | |
|
366 | | /**
|
367 | | * Generates a random database prefix and runs the install scripts on the prefixed database.
|
368 | | * After installation many caches are flushed and the internal browser is setup so that the page
|
369 | | * requests will run on the new prefix.
|
370 | | */
|
371 | | function setUp() {
|
372 | 1 | global $db_prefix, $simpletest_ua_key;
|
373 | 1 | if ($simpletest_ua_key) {
|
374 | 1 | $this->db_prefix_original = $db_prefix;
|
375 | 1 | $clean_url_original = variable_get('clean_url', 0);
|
376 | 1 | $db_prefix = 'simpletest'. mt_rand(1000, 1000000);
|
377 | | include_once './includes/install.inc';
|
378 | 1 | drupal_install_system();
|
379 | 1 | $module_list = drupal_verify_profile('default', 'en');
|
380 | 1 | drupal_install_modules($module_list);
|
381 | 1 | $task = 'profile';
|
382 | 1 | default_profile_tasks($task, '');
|
383 | 1 | menu_rebuild();
|
384 | 1 | actions_synchronize();
|
385 | 1 | _drupal_flush_css_js();
|
386 | 1 | variable_set('install_profile', 'default');
|
387 | 1 | variable_set('install_task', 'profile-finished');
|
388 | 1 | variable_set('clean_url', $clean_url_original);
|
389 | | }
|
390 | 1 | parent::setUp();
|
391 | | }
|
392 | |
|
393 | | /**
|
394 | | * Delete the tables created by setUp() and reset the database prefix.
|
395 | | */
|
396 | | function tearDown() {
|
397 | 1 | global $db_prefix;
|
398 | 1 | if (preg_match('/simpletest\d+/', $db_prefix)) {
|
399 | 1 | $schema = drupal_get_schema(NULL, TRUE);
|
400 | | $ret = array();
|
401 | 1 | foreach ($schema as $name => $table) {
|
402 | 1 | db_drop_table($ret, $name);
|
403 | | }
|
404 | 1 | $db_prefix = $this->db_prefix_original;
|
405 | 1 | $this->_logged_in = FALSE;
|
406 | 1 | $this->_modules = $this->_originalModules;
|
407 | 1 | $this->curlClose();
|
408 | | }
|
409 | 1 | parent::tearDown();
|
410 | | }
|
411 | |
|
412 | | /**
|
413 | | * Set necessary reporter info.
|
414 | | */
|
415 | | function run(&$reporter) {
|
416 | | $arr = array('class' => get_class($this));
|
417 | 1 | if (method_exists($this, 'getInfo')) {
|
418 | 1 | $arr = array_merge($arr, $this->getInfo());
|
419 | | }
|
420 | 1 | $reporter->test_info_stack[] = $arr;
|
421 | 1 | parent::run($reporter);
|
422 | 1 | array_pop($reporter->test_info_stack);
|
423 | | }
|
424 | |
|
425 | | /**
|
426 | | * Initializes the cURL connection and gets a session cookie.
|
427 | | *
|
428 | | * This function will add authentaticon headers as specified in
|
429 | | * simpletest_httpauth_username and simpletest_httpauth_pass variables.
|
430 | | * Also, see the description of $curl_options among the properties.
|
431 | | */
|
432 | | protected function curlConnect() {
|
433 | 1 | global $base_url, $db_prefix, $simpletest_ua_key;
|
434 | 1 | if (!isset($this->ch)) {
|
435 | 1 | $this->ch = curl_init();
|
436 | 1 | $curl_options = $this->curl_options + array(
|
437 | | CURLOPT_COOKIEJAR => $this->cookie_file,
|
438 | | CURLOPT_URL => $base_url,
|
439 | | CURLOPT_FOLLOWLOCATION => TRUE,
|
440 | | CURLOPT_RETURNTRANSFER => TRUE,
|
441 | | );
|
442 | 1 | if (preg_match('/simpletest\d+/', $db_prefix)) {
|
443 | 1 | $curl_options[CURLOPT_USERAGENT] = $db_prefix .','. $simpletest_ua_key;
|
444 | | }
|
445 | 1 | if (!isset($curl_options[CURLOPT_USERPWD]) && ($auth = variable_get('simpletest_httpauth_username', ''))) {
|
446 | | if ($pass = variable_get('simpletest_httpauth_pass', '')) {
|
447 | | $auth .= ':'. $pass;
|
448 | | }
|
449 | | $curl_options[CURLOPT_USERPWD] = $auth;
|
450 | | }
|
451 | 1 | return $this->curlExec($curl_options);
|
452 | | }
|
453 | | }
|
454 | |
|
455 | | /**
|
456 | | * Peforms a cURL exec with the specified options after calling curlConnect().
|
457 | | *
|
458 | | * @param array $curl_options Custom cURL options.
|
459 | | * @return string Content returned from the exec.
|
460 | | */
|
461 | | protected function curlExec($curl_options) {
|
462 | 1 | $this->curlConnect();
|
463 | 1 | $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
|
464 | 1 | curl_setopt_array($this->ch, $this->curl_options + $curl_options);
|
465 | 1 | $this->_content = curl_exec($this->ch);
|
466 | 1 | $this->plain_text = FALSE;
|
467 | 1 | $this->elements = FALSE;
|
468 | 1 | $this->assertTrue($this->_content, t(' [browser] !method to !url, response is !length bytes.', array('!method' => isset($curl_options[CURLOPT_POSTFIELDS]) ? 'POST' : 'GET', '!url' => $url, '!length' => strlen($this->_content))));
|
469 | 1 | return $this->_content;
|
470 | | }
|
471 | |
|
472 | | /**
|
473 | | * Close the cURL handler and unset the handler.
|
474 | | */
|
475 | | protected function curlClose() {
|
476 | 1 | if (isset($this->ch)) {
|
477 | 1 | curl_close($this->ch);
|
478 | 1 | unset($this->ch);
|
479 | | }
|
480 | | }
|
481 | |
|
482 | | /**
|
483 | | * Parse content returned from curlExec using DOM and simplexml.
|
484 | | *
|
485 | | * @return SimpleXMLElement A SimpleXMLElement or FALSE on failure.
|
486 | | */
|
487 | | protected function parse() {
|
488 | 1 | if (!$this->elements) {
|
489 | | // DOM can load HTML soup. But, HTML soup can throw warnings, supress
|
490 | | // them.
|
491 | 1 | @$htmlDom = DOMDocument::loadHTML($this->_content);
|
492 | 1 | if ($htmlDom) {
|
493 | 1 | $this->assertTrue(TRUE, t(' [browser] Valid HTML found on "@path"', array('@path' => $this->getUrl())));
|
494 | | // It's much easier to work with simplexml than DOM, luckily enough
|
495 | | // we can just simply import our DOM tree.
|
496 | 1 | $this->elements = simplexml_import_dom($htmlDom);
|
497 | | }
|
498 | | }
|
499 | 1 | return $this->elements;
|
500 | | }
|
501 | |
|
502 | | /**
|
503 | | * Retrieves a Drupal path or an absolute path.
|
504 | | *
|
505 | | * @param $path string Drupal path or url to load into internal browser
|
506 | | * @param array $options Options to be forwarded to url().
|
507 | | * @return The retrieved HTML string, also available as $this->drupalGetContent()
|
508 | | */
|
509 | | function drupalGet($path, $options = array()) {
|
510 | 1 | $options['absolute'] = TRUE;
|
511 | 1 | return $this->curlExec(array(CURLOPT_URL => url($path, $options)));
|
512 | | }
|
513 | |
|
514 | | /**
|
515 | | * Do a post request on a drupal page.
|
516 | | * It will be done as usual post request with SimpleBrowser
|
517 | | * By $reporting you specify if this request does assertions or not
|
518 | | * Warning: empty ("") returns will cause fails with $reporting
|
519 | | *
|
520 | | * @param string $path
|
521 | | * Location of the post form. Either a Drupal path or an absolute path or
|
522 | | * NULL to post to the current page.
|
523 | | * @param array $edit
|
524 | | * Field data in an assocative array. Changes the current input fields
|
525 | | * (where possible) to the values indicated. A checkbox can be set to
|
526 | | * TRUE to be checked and FALSE to be unchecked.
|
527 | | * @param string $submit
|
528 | | * Untranslated value, id or name of the submit button.
|
529 | | * @param $tamper
|
530 | | * If this is set to TRUE then you can post anything, otherwise hidden and
|
531 | | * nonexistent fields are not posted.
|
532 | | */
|
533 | | function drupalPost($path, $edit, $submit, $tamper = FALSE) {
|
534 | 1 | $submit_matches = FALSE;
|
535 | 1 | if (isset($path)) {
|
536 | 1 | $html = $this->drupalGet($path);
|
537 | | }
|
538 | 1 | if ($this->parse()) {
|
539 | 1 | $edit_save = $edit;
|
540 | | // Let's iterate over all the forms.
|
541 | 1 | $forms = $this->elements->xpath('//form');
|
542 | 1 | foreach ($forms as $form) {
|
543 | 1 | if ($tamper) {
|
544 | | // @TODO: this will be Drupal specific. One needs to add the build_id
|
545 | | // and the token to $edit then $post that.
|
546 | | }
|
547 | | else {
|
548 | | // We try to set the fields of this form as specified in $edit.
|
549 | 1 | $edit = $edit_save;
|
550 | | $post = array();
|
551 | | $upload = array();
|
552 | 1 | $submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
|
553 | 1 | $action = isset($form['action']) ? $this->getAbsoluteUrl($form['action']) : $this->getUrl();
|
554 | | }
|
555 | | // We post only if we managed to handle every field in edit and the
|
556 | | // submit button matches;
|
557 | 1 | if (!$edit && $submit_matches) {
|
558 | | // This part is not pretty. There is very little I can do.
|
559 | 1 | if ($upload) {
|
560 | | foreach ($post as &$value) {
|
561 | | if (strlen($value) > 0 && $value[0] == '@') {
|
562 | | $this->fail(t("Can't upload and post a value starting with @"));
|
563 | | return FALSE;
|
564 | | }
|
565 | | }
|
566 | | foreach ($upload as $key => $file) {
|
567 | | $post[$key] = '@'. realpath($file);
|
568 | | }
|
569 | | }
|
570 | | else {
|
571 | 1 | $post_array = $post;
|
572 | | $post = array();
|
573 | 1 | foreach ($post_array as $key => $value) {
|
574 | | // Whethet this needs to be urlencode or rawurlencode, is not
|
575 | | // quite clear, but this seems to be the better choice.
|
576 | 1 | $post[] = urlencode($key) .'='. urlencode($value);
|
577 | | }
|
578 | 1 | $post = implode('&', $post);
|
579 | | }
|
580 | 1 | return $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POSTFIELDS => $post));
|
581 | | }
|
582 | | }
|
583 | | // We have not found a form which contained all fields of $edit.
|
584 | | $this->fail(t('Found the requested form'));
|
585 | | $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
|
586 | | foreach ($edit as $name => $value) {
|
587 | | $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
|
588 | | }
|
589 | | }
|
590 | | }
|
591 | |
|
592 | | /**
|
593 | | * Handle form input related to drupalPost(). Ensure that the specified fields
|
594 | | * exist and attempt to create POST data in the correct manor for the particular
|
595 | | * field type.
|
596 | | *
|
597 | | * @param array $post Reference to array of post values.
|
598 | | * @param array $edit Reference to array of edit values to be checked against the form.
|
599 | | * @param string $submit Form submit button value.
|
600 | | * @param array $form Array of form elements.
|
601 | | * @return boolean Submit value matches a valid submit input in the form.
|
602 | | */
|
603 | | protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
|
604 | | // Retrieve the form elements.
|
605 | 1 | $elements = $form->xpath('.//input|.//textarea|.//select');
|
606 | 1 | $submit_matches = FALSE;
|
607 | 1 | foreach ($elements as $element) {
|
608 | | // SimpleXML objects need string casting all the time.
|
609 | 1 | $name = (string)$element['name'];
|
610 | | // This can either be the type of <input> or the name of the tag itself
|
611 | | // for <select> or <textarea>.
|
612 | 1 | $type = isset($element['type']) ? (string)$element['type'] : $element->getName();
|
613 | 1 | $value = isset($element['value']) ? (string)$element['value'] : '';
|
614 | 1 | $done = FALSE;
|
615 | 1 | if (isset($edit[$name])) {
|
616 | | switch ($type) {
|
617 | 1 | case 'text':
|
618 | 1 | case 'textarea':
|
619 | 1 | case 'password':
|
620 | 1 | $post[$name] = $edit[$name];
|
621 | 1 | unset($edit[$name]);
|
622 | 1 | break;
|
623 | | case 'radio':
|
624 | | if ($edit[$name] == $value) {
|
625 | | $post[$name] = $edit[$name];
|
626 | | unset($edit[$name]);
|
627 | | }
|
628 | | break;
|
629 | | case 'checkbox':
|
630 | | // To prevent checkbox from being checked.pass in a FALSE,
|
631 | | // otherwise the checkbox will be set to its value regardless
|
632 | | // of $edit.
|
633 | | if ($edit[$name] === FALSE) {
|
634 | | unset($edit[$name]);
|
635 | | continue 2;
|
636 | | }
|
637 | | else {
|
638 | | unset($edit[$name]);
|
639 | | $post[$name] = $value;
|
640 | | }
|
641 | | break;
|
642 | | case 'select':
|
643 | | $new_value = $edit[$name];
|
644 | | $index = 0;
|
645 | | $key = preg_replace('/\[\]$/', '', $name);
|
646 | | foreach ($element->option as $option) {
|
647 | | if (is_array($new_value)) {
|
648 | | $option_value= (string)$option['value'];
|
649 | | if (in_array($option_value, $new_value)) {
|
650 | | $post[$key .'['. $index++ .']'] = $option_value;
|
651 | | $done = TRUE;
|
652 | | unset($edit[$name]);
|
653 | | }
|
654 | | }
|
655 | | elseif ($new_value == $option['value']) {
|
656 | | $post[$name] = $new_value;
|
657 | | unset($edit[$name]);
|
658 | | $done = TRUE;
|
659 | | }
|
660 | | }
|
661 | | break;
|
662 | | case 'file':
|
663 | | $upload[$name] = $edit[$name];
|
664 | | unset($edit[$name]);
|
665 | | break;
|
666 | | }
|
667 | | }
|
668 | 1 | if (!isset($post[$name]) && !$done) {
|
669 | | switch ($type) {
|
670 | 1 | case 'textarea':
|
671 | | $post[$name] = (string)$element;
|
672 | | break;
|
673 | 1 | case 'select':
|
674 | | $single = empty($element['multiple']);
|
675 | | $first = TRUE;
|
676 | | $index = 0;
|
677 | | $key = preg_replace('/\[\]$/', '', $name);
|
678 | | foreach ($element->option as $option) {
|
679 | | // For single select, we load the first option, if there is a
|
680 | | // selected option that will overwrite it later.
|
681 | | if ($option['selected'] || (!$first && $single)) {
|
682 | | $first = FALSE;
|
683 | | if ($single) {
|
684 | | $post[$name] = (string)$option['value'];
|
685 | | }
|
686 | | else {
|
687 | | $post[$key .'['. $index++ .']'] = (string)$option['value'];
|
688 | | }
|
689 | | }
|
690 | | }
|
691 | | break;
|
692 | 1 | case 'file':
|
693 | | break;
|
694 | 1 | case 'submit':
|
695 | 1 | case 'image':
|
696 | 1 | if ($submit == $value) {
|
697 | 1 | $post[$name] = $value;
|
698 | 1 | $submit_matches = TRUE;
|
699 | | }
|
700 | 1 | break;
|
701 | 1 | case 'radio':
|
702 | 1 | case 'checkbox':
|
703 | | if (!isset($element['checked'])) {
|
704 | | break;
|
705 | | }
|
706 | | // Deliberate no break.
|
707 | 1 | default:
|
708 | 1 | $post[$name] = $value;
|
709 | | }
|
710 | | }
|
711 | | }
|
712 | 1 | return $submit_matches;
|
713 | | }
|
714 | |
|
715 | | /**
|
716 | | * Follows a link by name.
|
717 | | *
|
718 | | * Will click the first link found with this link text by default, or a
|
719 | | * later one if an index is given. Match is case insensitive with
|
720 | | * normalized space. The label is translated label. There is an assert
|
721 | | * for successful click.
|
722 | | * WARNING: Assertion fails on empty ("") output from the clicked link.
|
723 | | *
|
724 | | * @param string $label Text between the anchor tags.
|
725 | | * @param integer $index Link position counting from zero.
|
726 | | * @param boolean $reporting Assertions or not.
|
727 | | * @return boolean/string Page on success.
|
728 | | */
|
729 | | function clickLink($label, $index = 0) {
|
730 | | $url_before = $this->getUrl();
|
731 | | $ret = FALSE;
|
732 | | if ($this->parse()) {
|
733 | | $urls = $this->elements->xpath('//a[text()="'. $label .'"]');
|
734 | | if (isset($urls[$index])) {
|
735 | | $url_target = $this->getAbsoluteUrl($urls[$index]['href']);
|
736 | | $curl_options = array(CURLOPT_URL => $url_target);
|
737 | | $ret = $this->curlExec($curl_options);
|
738 | | }
|
739 | | $this->assertTrue($ret, " [browser] clicked link $label ($url_target) from $url_before");
|
740 | | }
|
741 | | return $ret;
|
742 | | }
|
743 | |
|
744 | | /**
|
745 | | * Takes a path and returns an absolute path.
|
746 | | *
|
747 | | * @param @path
|
748 | | * The path, can be a Drupal path or a site-relative path. It might have a
|
749 | | * query, too. Can even be an absolute path which is just passed through.
|
750 | | * @return
|
751 | | * An absolute path.
|
752 | | */
|
753 | | function getAbsoluteUrl($path) {
|
754 | | $options = array('absolute' => TRUE);
|
755 | 1 | $parts = parse_url($path);
|
756 | | // This is more crude than the menu_is_external but enough here.
|
757 | 1 | if (empty($parts['host'])) {
|
758 | 1 | $path = $parts['path'];
|
759 | 1 | $base_path = base_path();
|
760 | 1 | $n = strlen($base_path);
|
761 | 1 | if (substr($path, 0, $n) == $base_path) {
|
762 | 1 | $path = substr($path, $n);
|
763 | | }
|
764 | 1 | if (isset($parts['query'])) {
|
765 | | $options['query'] = $parts['query'];
|
766 | | }
|
767 | 1 | $path = url($path, $options);
|
768 | | }
|
769 | 1 | return $path;
|
770 | | }
|
771 | |
|
772 | | /**
|
773 | | * Get the current url from the cURL handler.
|
774 | | *
|
775 | | * @return string current url.
|
776 | | */
|
777 | | function getUrl() {
|
778 | 1 | return curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL);
|
779 | | }
|
780 | |
|
781 | | /**
|
782 | | * Gets the current raw HTML of requested page.
|
783 | | */
|
784 | | function drupalGetContent() {
|
785 | | return $this->_content;
|
786 | | }
|
787 | |
|
788 | | /**
|
789 | | * Pass if the raw text IS found on the loaded page, fail otherwise. Raw text
|
790 | | * refers to the raw HTML that the page generated.
|
791 | | *
|
792 | | * @param string $raw Raw string to look for.
|
793 | | * @param string $message Message to display.
|
794 | | * @return boolean TRUE on pass.
|
795 | | */
|
796 | | function assertRaw($raw, $message = "%s") {
|
797 | 1 | return $this->assertFalse(strpos($this->_content, $raw) === FALSE, $message);
|
798 | | }
|
799 | |
|
800 | | /**
|
801 | | * Pass if the raw text is NOT found on the loaded page, fail otherwise. Raw text
|
802 | | * refers to the raw HTML that the page generated.
|
803 | | *
|
804 | | * @param string $raw Raw string to look for.
|
805 | | * @param string $message Message to display.
|
806 | | * @return boolean TRUE on pass.
|
807 | | */
|
808 | | function assertNoRaw($raw, $message = "%s") {
|
809 | | return $this->assertTrue(strpos($this->_content, $raw) === FALSE, $message);
|
810 | | }
|
811 | |
|
812 | |
|
813 | | /**
|
814 | | * Pass if the text IS found on the text version of the page. The text version
|
815 | | * is the equivilent of what a user would see when viewing through a web browser.
|
816 | | * In other words the HTML has been filtered out of the contents.
|
817 | | *
|
818 | | * @param string $raw Text string to look for.
|
819 | | * @param string $message Message to display.
|
820 | | * @return boolean TRUE on pass.
|
821 | | */
|
822 | | function assertText($text, $message = '') {
|
823 | 1 | return $this->assertTextHelper($text, $message, FALSE);
|
824 | | }
|
825 | |
|
826 | | /**
|
827 | | * Pass if the text is NOT found on the text version of the page. The text version
|
828 | | * is the equivilent of what a user would see when viewing through a web browser.
|
829 | | * In other words the HTML has been filtered out of the contents.
|
830 | | *
|
831 | | * @param string $raw Text string to look for.
|
832 | | * @param string $message Message to display.
|
833 | | * @return boolean TRUE on pass.
|
834 | | */
|
835 | | function assertNoText($text, $message = '') {
|
836 | 1 | return $this->assertTextHelper($text, $message, TRUE);
|
837 | | }
|
838 | |
|
839 | | /**
|
840 | | * Filter out the HTML of the page and assert that the plain text us found. Called by
|
841 | | * the plain text assertions.
|
842 | | *
|
843 | | * @param string $text Text to look for.
|
844 | | * @param string $message Message to display.
|
845 | | * @param boolean $not_exists The assert to make in relation to the text's existance.
|
846 | | * @return boolean Assertion result.
|
847 | | */
|
848 | | protected function assertTextHelper($text, $message, $not_exists) {
|
849 | 1 | if ($this->plain_text === FALSE) {
|
850 | 1 | $this->plain_text = filter_xss($this->_content, array());
|
851 | | }
|
852 | 1 | if (!$message) {
|
853 | | $message = '"'. $text .'"'. ($not_exists ? ' not found.' : ' found.');
|
854 | | }
|
855 | 1 | return $this->assertTrue($not_exists == (strpos($this->plain_text, $text) === FALSE), $message);
|
856 | | }
|
857 | |
|
858 | | /**
|
859 | | * Will trigger a pass if the Perl regex pattern is found in the raw content.
|
860 | | *
|
861 | | * @param string $pattern Perl regex to look for including the regex delimiters.
|
862 | | * @param string $message Message to display.
|
863 | | * @return boolean True if pass.
|
864 | | */
|
865 | | function assertPattern($pattern, $message = '%s') {
|
866 | | return $this->assert(new PatternExpectation($pattern), $this->drupalGetContent(), $message);
|
867 | | }
|
868 | |
|
869 | | /**
|
870 | | * Will trigger a pass if the perl regex pattern is not present in raw content.
|
871 | | *
|
872 | | * @param string $pattern Perl regex to look for including the regex delimiters.
|
873 | | * @param string $message Message to display.
|
874 | | * @return boolean True if pass.
|
875 | | */
|
876 | | function assertNoPattern($pattern, $message = '%s') {
|
877 | | return $this->assert(new NoPatternExpectation($pattern), $this->drupalGetContent(), $message);
|
878 | | }
|
879 | |
|
880 | | /**
|
881 | | * Pass if the page title is the given string.
|
882 | | *
|
883 | | * @param $title Text string to look for.
|
884 | | * @param $message Message to display.
|
885 | | * @return boolean TRUE on pass.
|
886 | | */
|
887 | | function assertTitle($title, $message) {
|
888 | 1 | return $this->assertTrue($this->parse() && $this->elements->xpath('//title[text()="'. $title .'"]'), $message);
|
889 | | }
|
890 | |
|
891 | | /**
|
892 | | * Assert that a field exists in the current page by the given XPath.
|
893 | | *
|
894 | | * @param string $xpath XPath used to find the field.
|
895 | | * @param string $value Value of the field to assert.
|
896 | | * @param string $message Message to display.
|
897 | | * @return boolean Assertion result.
|
898 | | */
|
899 | | function assertFieldByXPath($xpath, $value, $message) {
|
900 | | $fields = array();
|
901 | 1 | if ($this->parse()) {
|
902 | 1 | $fields = $this->elements->xpath($xpath);
|
903 | | }
|
904 | |
|
905 | | // If value specified then check array for match.
|
906 | 1 | $found = TRUE;
|
907 | 1 | if ($value) {
|
908 | | $found = FALSE;
|
909 | | foreach ($fields as $field) {
|
910 | | if ($field['value'] == $value) {
|
911 | | $found = TRUE;
|
912 | | }
|
913 | | }
|
914 | | }
|
915 | 1 | return $this->assertTrue($fields && $found, $message);
|
916 | | }
|
917 | |
|
918 | | /**
|
919 | | * Assert that a field does not exists in the current page by the given XPath.
|
920 | | *
|
921 | | * @param string $xpath XPath used to find the field.
|
922 | | * @param string $value Value of the field to assert.
|
923 | | * @param string $message Message to display.
|
924 | | * @return boolean Assertion result.
|
925 | | */
|
926 | | function assertNoFieldByXPath($xpath, $value, $message) {
|
927 | | $fields = array();
|
928 | | if ($this->parse()) {
|
929 | | $fields = $this->elements->xpath($xpath);
|
930 | | }
|
931 | |
|
932 | | // If value specified then check array for match.
|
933 | | $found = TRUE;
|
934 | | if ($value) {
|
935 | | $found = FALSE;
|
936 | | foreach ($fields as $field) {
|
937 | | if ($field['value'] == $value) {
|
938 | | $found = TRUE;
|
939 | | }
|
940 | | }
|
941 | | }
|
942 | | return $this->assertFalse($fields && $found, $message);
|
943 | | }
|
944 | |
|
945 | | /**
|
946 | | * Assert that a field exists in the current page with the given name and value.
|
947 | | *
|
948 | | * @param string $name Name of field to assert.
|
949 | | * @param string $value Value of the field to assert.
|
950 | | * @param string $message Message to display.
|
951 | | * @return boolean Assertion result.
|
952 | | */
|
953 | | function assertFieldByName($name, $value = '', $message = '') {
|
954 | | return $this->assertFieldByXPath($this->_constructFieldXpath('name', $name), $value, $message ? $message : t(' [browser] found field by name @name', array('@name' => $name)));
|
955 | | }
|
956 | |
|
957 | | /**
|
958 | | * Assert that a field does not exists in the current page with the given name and value.
|
959 | | *
|
960 | | * @param string $name Name of field to assert.
|
961 | | * @param string $value Value of the field to assert.
|
962 | | * @param string $message Message to display.
|
963 | | * @return boolean Assertion result.
|
964 | | */
|
965 | | function assertNoFieldByName($name, $value = '', $message = '') {
|
966 | | return $this->assertNoFieldByXPath($this->_constructFieldXpath('name', $name), $value, $message ? $message : t(' [browser] did not find field by name @name', array('@name' => $name)));
|
967 | | }
|
968 | |
|
969 | | /**
|
970 | | * Assert that a field exists in the current page with the given id and value.
|
971 | | *
|
972 | | * @param string $id Id of field to assert.
|
973 | | * @param string $value Value of the field to assert.
|
974 | | * @param string $message Message to display.
|
975 | | * @return boolean Assertion result.
|
976 | | */
|
977 | | function assertFieldById($id, $value = '', $message = '') {
|
978 | | return $this->assertFieldByXPath($this->_constructFieldXpath('id', $id), $value, $message ? $message : t(' [browser] found field by id @id', array('@id' => $id)));
|
979 | | }
|
980 | |
|
981 | | /**
|
982 | | * Assert that a field does not exists in the current page with the given id and value.
|
983 | | *
|
984 | | * @param string $id Id of field to assert.
|
985 | | * @param string $value Value of the field to assert.
|
986 | | * @param string $message Message to display.
|
987 | | * @return boolean Assertion result.
|
988 | | */
|
989 | | function assertNoFieldById($id, $value = '', $message = '') {
|
990 | | return $this->assertNoFieldByXPath($this->_constructFieldXpath('id', $id), $value, $message ? $message : t(' [browser] did not find field by id @id', array('@id' => $id)));
|
991 | | }
|
992 | |
|
993 | | /**
|
994 | | * Assert that a field exists in the current page with the given name or id.
|
995 | | *
|
996 | | * @param string $field Name or id of the field.
|
997 | | * @param string $message Message to display.
|
998 | | * @return boolean Assertion result.
|
999 | | */
|
1000 | | function assertField($field, $message = '') {
|
1001 | 1 | return $this->assertFieldByXPath($this->_constructFieldXpath('name', $field) .'|'. $this->_constructFieldXpath('id', $field), '', $message);
|
1002 | | }
|
1003 | |
|
1004 | | /**
|
1005 | | * Assert that a field does not exists in the current page with the given name or id.
|
1006 | | *
|
1007 | | * @param string $field Name or id of the field.
|
1008 | | * @param string $message Message to display.
|
1009 | | * @return boolean Assertion result.
|
1010 | | */
|
1011 | | function assertNoField($field, $message = '') {
|
1012 | | return $this->assertNoFieldByXPath($this->_constructFieldXpath('name', $field) .'|'. $this->_constructFieldXpath('id', $field), '', $message);
|
1013 | | }
|
1014 | |
|
1015 | | /**
|
1016 | | * Construct an XPath for the given set of attributes and value.
|
1017 | | *
|
1018 | | * @param array $attribute Field attributes.
|
1019 | | * @param string $value Value of field.
|
1020 | | * @return string XPath for specified values.
|
1021 | | */
|
1022 | | function _constructFieldXpath($attribute, $value) {
|
1023 | 1 | return '//textarea[@'. $attribute .'="'. $value .'"]|//input[@'. $attribute .'="'. $value .'"]|//select[@'. $attribute .'="'. $value .'"]';
|
1024 | | }
|
1025 | |
|
1026 | | /**
|
1027 | | * Assert the page responds with the specified response code.
|
1028 | | *
|
1029 | | * @param integer $code Reponse code. For example 200 is a successful page request. For
|
1030 | | * a list of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
|
1031 | | * @param string $message Message to display.
|
1032 | | * @return boolean Assertion result.
|
1033 | | */
|
1034 | | function assertResponse($code, $message = '') {
|
1035 | 1 | $curl_code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
|
1036 | 1 | $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code;
|
1037 | 1 | return $this->assertTrue($match, $message ? $message : t(' [browser] HTTP response expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)));
|
1038 | | }
|
1039 | | }
|