Spike PHPCoverage Details: drupal_test_case.php

Line #FrequencySource 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) {
261    if (!$label) {
271      if (method_exists($this, 'getInfo')) {
281        $info  = $this->getInfo();
291        $label = $info['name'];
30       }
31     }
321    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_') {
1601    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
1611    for ($x = 0; $x < $number; $x++) {
1621      $prefix .= $chars{mt_rand(0, strlen($chars)-1)};
1631      if ($x == 0) {
1641        $chars .= '0123456789';
165       }
166     }
1671    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.
2631    $rid = $this->_drupalCreateRole($permissions);
2641    if (!$rid) {
265       return FALSE;
266     }
267 
268     // Create a user assigned to that role.
269     $edit = array();
2701    $edit['name']   = $this->randomName();
2711    $edit['mail']   = $edit['name'] .'@example.com';
272     $edit['roles']  = array($rid => $rid);
2731    $edit['pass']   = user_password();
2741    $edit['status'] = 1;
275 
2761    $account = user_save('', $edit);
277 
2781    $this->assertTrue(!empty($account->uid), " [user] name: $edit[name] pass: $edit[pass] created");
2791    if (empty($account->uid)) {
280       return FALSE;
281     }
282 
283     // Add the raw password so that we can log in as this user.
2841    $account->pass_raw = $edit['pass'];
2851    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.
2961    if ($permissions === NULL) {
297       $permission_string = 'access comments, access content, post comments, post comments without approval';
298     } else {
2991      $permission_string = implode(', ', $permissions);
300     }
301 
302     // Create new role.
3031    $role_name = $this->randomName();
3041    db_query("INSERT INTO {role} (name) VALUES ('%s')", $role_name);
3051    $role = db_fetch_object(db_query("SELECT * FROM {role} WHERE name = '%s'", $role_name));
3061    $this->assertTrue($role, " [role] created name: $role_name, id: " . (isset($role->rid) ? $role->rid : t('-n/a-')));
3071    if ($role && !empty($role->rid)) {
308       // Assign permissions to role and mark it for clean-up.
3091      db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')", $role->rid, $permission_string);
3101      $this->assertTrue(db_affected_rows(), ' [role] created permissions: ' . $permission_string);
3111      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) {
3281    if ($this->_logged_in) {
3291      $this->drupalLogout();
330     }
331 
3321    if (!isset($user)) {
333       $user = $this->_drupalCreateRole();
334     }
335 
336     $edit = array(
3371      'name' => $user->name,
338       'pass' => $user->pass_raw
339     );
3401    $this->drupalPost('user', $edit, t('Log in'));
341 
3421    $pass = $this->assertText($user->name, ' [login] found name: '. $user->name);
3431    $pass = $pass && $this->assertNoText(t('The username %name has been blocked.', array('%name' => $user->name)), ' [login] not blocked');
3441    $pass = $pass && $this->assertNoText(t('The name %name is a reserved username.', array('%name' => $user->name)), ' [login] not reserved');
345 
3461    $this->_logged_in = $pass;
347 
3481    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.
3561    $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.
3591    $this->drupalGet('user');
3601    $pass = $this->assertField('name', t('[logout] Username field found.'));
3611    $pass = $pass && $this->assertField('pass', t('[logout] Password field found.'));
362 
3631    $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() {
3721    global $db_prefix, $simpletest_ua_key;
3731    if ($simpletest_ua_key) {
3741      $this->db_prefix_original = $db_prefix;
3751      $clean_url_original = variable_get('clean_url', 0);
3761      $db_prefix = 'simpletest'. mt_rand(1000, 1000000);
377       include_once './includes/install.inc';
3781      drupal_install_system();
3791      $module_list = drupal_verify_profile('default', 'en');
3801      drupal_install_modules($module_list);
3811      $task = 'profile';
3821      default_profile_tasks($task, '');
3831      menu_rebuild();
3841      actions_synchronize();
3851      _drupal_flush_css_js();
3861      variable_set('install_profile', 'default');
3871      variable_set('install_task', 'profile-finished');
3881      variable_set('clean_url', $clean_url_original);
389     }
3901    parent::setUp();
391   }
392 
393   /**
394    * Delete the tables created by setUp() and reset the database prefix.
395    */
396   function tearDown() {
3971    global $db_prefix;
3981    if (preg_match('/simpletest\d+/', $db_prefix)) {
3991      $schema = drupal_get_schema(NULL, TRUE);
400       $ret = array();
4011      foreach ($schema as $name => $table) {
4021        db_drop_table($ret, $name);
403       }
4041      $db_prefix = $this->db_prefix_original;
4051      $this->_logged_in = FALSE;
4061      $this->_modules = $this->_originalModules;
4071      $this->curlClose();
408     }
4091    parent::tearDown();
410   }
411 
412   /**
413    * Set necessary reporter info.
414    */
415   function run(&$reporter) {
416     $arr = array('class' => get_class($this));
4171    if (method_exists($this, 'getInfo')) {
4181      $arr = array_merge($arr, $this->getInfo());
419     }
4201    $reporter->test_info_stack[] = $arr;
4211    parent::run($reporter);
4221    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() {
4331    global $base_url, $db_prefix, $simpletest_ua_key;
4341    if (!isset($this->ch)) {
4351      $this->ch = curl_init();
4361      $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       );
4421      if (preg_match('/simpletest\d+/', $db_prefix)) {
4431        $curl_options[CURLOPT_USERAGENT] = $db_prefix .','. $simpletest_ua_key;
444       }
4451      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       }
4511      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) {
4621    $this->curlConnect();
4631    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
4641    curl_setopt_array($this->ch, $this->curl_options + $curl_options);
4651    $this->_content = curl_exec($this->ch);
4661    $this->plain_text = FALSE;
4671    $this->elements = FALSE;
4681    $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))));
4691    return $this->_content;
470   }
471 
472   /**
473    * Close the cURL handler and unset the handler.
474    */
475   protected function curlClose() {
4761    if (isset($this->ch)) {
4771      curl_close($this->ch);
4781      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() {
4881    if (!$this->elements) {
489       // DOM can load HTML soup. But, HTML soup can throw warnings, supress
490       // them.
4911      @$htmlDom = DOMDocument::loadHTML($this->_content);
4921      if ($htmlDom) {
4931        $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.
4961        $this->elements = simplexml_import_dom($htmlDom);
497       }
498     }
4991    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()) {
5101    $options['absolute'] = TRUE;
5111    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) {
5341    $submit_matches = FALSE;
5351    if (isset($path)) {
5361      $html = $this->drupalGet($path);
537     }
5381    if ($this->parse()) {
5391      $edit_save = $edit;
540       // Let's iterate over all the forms.
5411      $forms = $this->elements->xpath('//form');
5421      foreach ($forms as $form) {
5431        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.
5491          $edit = $edit_save;
550           $post = array();
551           $upload = array();
5521          $submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
5531          $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;
5571        if (!$edit && $submit_matches) {
558           // This part is not pretty. There is very little I can do.
5591          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 {
5711            $post_array = $post;
572             $post = array();
5731            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.
5761              $post[] = urlencode($key) .'='. urlencode($value);
577             }
5781            $post = implode('&', $post);
579           }
5801          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.
6051    $elements = $form->xpath('.//input|.//textarea|.//select');
6061    $submit_matches = FALSE;
6071    foreach ($elements as $element) {
608       // SimpleXML objects need string casting all the time.
6091      $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>.
6121      $type = isset($element['type']) ? (string)$element['type'] : $element->getName();
6131      $value = isset($element['value']) ? (string)$element['value'] : '';
6141      $done = FALSE;
6151      if (isset($edit[$name])) {
616         switch ($type) {
6171          case 'text':
6181          case 'textarea':
6191          case 'password':
6201            $post[$name] = $edit[$name];
6211            unset($edit[$name]);
6221            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       }
6681      if (!isset($post[$name]) && !$done) {
669         switch ($type) {
6701          case 'textarea':
671             $post[$name] = (string)$element;
672             break;
6731          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;
6921          case 'file':
693             break;
6941          case 'submit':
6951          case 'image':
6961            if ($submit == $value) {
6971              $post[$name] = $value;
6981              $submit_matches = TRUE;
699             }
7001            break;
7011          case 'radio':
7021          case 'checkbox':
703             if (!isset($element['checked'])) {
704               break;
705             }
706             // Deliberate no break.
7071          default:
7081            $post[$name] = $value;
709         }
710       }
711     }
7121    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);
7551    $parts = parse_url($path);
756     // This is more crude than the menu_is_external but enough here.
7571    if (empty($parts['host'])) {
7581      $path = $parts['path'];
7591      $base_path = base_path();
7601      $n = strlen($base_path);
7611      if (substr($path, 0, $n) == $base_path) {
7621        $path = substr($path, $n);
763       }
7641      if (isset($parts['query'])) {
765         $options['query'] = $parts['query'];
766       }
7671      $path = url($path, $options);
768     }
7691    return $path;
770   }
771 
772   /**
773    * Get the current url from the cURL handler.
774    *
775    * @return string current url.
776    */
777   function getUrl() {
7781    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") {
7971    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 = '') {
8231    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 = '') {
8361    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) {
8491    if ($this->plain_text === FALSE) {
8501      $this->plain_text = filter_xss($this->_content, array());
851     }
8521    if (!$message) {
853       $message = '"'. $text .'"'. ($not_exists ? ' not found.' : ' found.');
854     }
8551    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) {
8881    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();
9011    if ($this->parse()) {
9021      $fields = $this->elements->xpath($xpath);
903     }
904 
905     // If value specified then check array for match.
9061    $found = TRUE;
9071    if ($value) {
908       $found = FALSE;
909       foreach ($fields as $field) {
910         if ($field['value'] == $value) {
911           $found = TRUE;
912         }
913       }
914     }
9151    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 = '') {
10011    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) {
10231    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 = '') {
10351    $curl_code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
10361    $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code;
10371    return $this->assertTrue($match, $message ? $message : t(' [browser] HTTP response expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)));
1038   }
1039 }