PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/sites/all/modules/simpletest/drupal_web_test_case.php

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