From 64fc9813b4a225bba8ab56bae0ca3b1db756121a Mon Sep 17 00:00:00 2001 From: Dries Buytaert <dries@buytaert.net> Date: Sun, 19 Sep 2010 18:39:18 +0000 Subject: [PATCH] - Patch #789186 by effulgentsia: improve drupalPostAJAX() to be used more easily, leading to better AJAX test coverage. (Actual commit so we have proper credit.) --- modules/simpletest/drupal_web_test_case.php | 210 +++++++++++++------- modules/simpletest/tests/ajax.test | 15 +- modules/simpletest/tests/form.test | 6 +- modules/simpletest/tests/form_test.file.inc | 13 +- 4 files changed, 159 insertions(+), 85 deletions(-) diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 36568a8d45f2..5b1849dd6425 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -689,6 +689,13 @@ class DrupalWebTestCase extends DrupalTestCase { */ protected $plainTextContent; + /** + * The value of the Drupal.settings JavaScript variable for the page currently loaded in the internal browser. + * + * @var Array + */ + protected $drupalSettings; + /** * The parsed version of the page. * @@ -1698,8 +1705,14 @@ protected function drupalGetAJAX($path, array $options = array(), array $headers * Note that this is not the Drupal $form_id, but rather the HTML ID of the * form, which is typically the same thing but with hyphens replacing the * underscores. - */ - protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL) { + * @param $extra_post + * (optional) A string of additional data to append to the POST submission. + * This can be used to add POST data for which there are no HTML fields, as + * is done by drupalPostAJAX(). This string is literally appended to the + * POST data, so it must already be urlencoded and contain a leading "&" + * (e.g., "&extra_var1=hello+world&extra_var2=you%26me"). + */ + protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { $submit_matches = FALSE; $ajax = is_array($submit); if (isset($path)) { @@ -1750,23 +1763,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 $post[$key] = urlencode($key) . '=' . urlencode($value); } - // For AJAX requests, add '_triggering_element_*' and - // 'ajax_html_ids' to the POST data, as ajax.js does. - if ($ajax) { - if (is_array($submit['triggering_element'])) { - // Get the first key/value pair in the array. - $post['_triggering_element_value'] = '_triggering_element_value=' . urlencode(reset($submit['triggering_element'])); - $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode(key($submit['triggering_element'])); - } - else { - $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode($submit['triggering_element']); - } - foreach ($this->xpath('//*[@id]') as $element) { - $id = (string) $element['id']; - $post[] = urlencode('ajax_html_ids[]') . '=' . urlencode($id); - } - } - $post = implode('&', $post); + $post = implode('&', $post) . $extra_post; } $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers)); // Ensure that any changes to variables in the other thread are picked up. @@ -1803,70 +1800,131 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a * * @see ajax.js */ - protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = array()) { + protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) { // Get the content of the initial page prior to calling drupalPost(), since // drupalPost() replaces $this->content. if (isset($path)) { $this->drupalGet($path, $options); } - $content = $this->drupalGetContent(); - $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id)); + $content = $this->content; + $drupal_settings = $this->drupalSettings; + + // Get the AJAX settings bound to the triggering element. + if (!isset($ajax_settings)) { + if (is_array($triggering_element)) { + $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]'; + } + else { + $xpath = '//*[@name="' . $triggering_element . '"]'; + } + if (isset($form_html_id)) { + $xpath = '//form[@id="' . $form_html_id . '"]' . $xpath; + } + $element = $this->xpath($xpath); + $element_id = (string) $element[0]['id']; + $ajax_settings = $drupal_settings['ajax'][$element_id]; + } + + // Add extra information to the POST data as ajax.js does. + $extra_post = ''; + if (isset($ajax_settings['submit'])) { + foreach ($ajax_settings['submit'] as $key => $value) { + $extra_post .= '&' . urlencode($key) . '=' . urlencode($value); + } + } + foreach ($this->xpath('//*[@id]') as $element) { + $id = (string) $element['id']; + $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id); + } - // We need $ajax_settings['wrapper'] to perform DOM manipulation. + // Submit the POST request. + $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); + + // Change the page content by applying the returned commands. if (!empty($ajax_settings) && !empty($return)) { + // ajax.js applies some defaults to the settings object, so do the same + // for what's used by this function. + $ajax_settings += array( + 'method' => 'replaceWith', + ); // DOM can load HTML soup. But, HTML soup can throw warnings, suppress // them. @$dom = DOMDocument::loadHTML($content); foreach ($return as $command) { - // @todo ajax.js can process commands other than 'insert' and can - // process commands that include a 'selector', but these are hard to - // emulate with DOMDocument. For now, we only implement 'insert' - // commands that use $ajax_settings['wrapper']. - if ($command['command'] == 'insert' && !isset($command['selector'])) { - // $dom->getElementById() doesn't work when drupalPostAJAX() is - // invoked multiple times for a page, so use XPath instead. This also - // sets us up for adding support for $command['selector'], though it - // will require transforming a jQuery selector to XPath. - $xpath = new DOMXPath($dom); - $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); - if ($wrapperNode) { - // ajax.js adds an enclosing DIV to work around a Safari bug. - $newDom = new DOMDocument(); - $newDom->loadHTML('<div>' . $command['data'] . '</div>'); - $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); - $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; - // The "method" is a jQuery DOM manipulation function. Emulate each - // one using PHP's DOMNode API. - switch ($method) { - case 'replaceWith': - $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); - break; - case 'append': - $wrapperNode->appendChild($newNode); - break; - case 'prepend': - // If no firstChild, insertBefore() falls back to appendChild(). - $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); - break; - case 'before': - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); - break; - case 'after': - // If no nextSibling, insertBefore() falls back to appendChild(). - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); - break; - case 'html': - foreach ($wrapperNode->childNodes as $childNode) { - $wrapperNode->removeChild($childNode); + switch ($command['command']) { + case 'settings': + $drupal_settings = array_merge_recursive($drupal_settings, $command['settings']); + break; + + case 'insert': + // @todo ajax.js can process commands that include a 'selector', but + // these are hard to emulate with DOMDocument. For now, we only + // implement 'insert' commands that use $ajax_settings['wrapper']. + if (!isset($command['selector'])) { + // $dom->getElementById() doesn't work when drupalPostAJAX() is + // invoked multiple times for a page, so use XPath instead. This + // also sets us up for adding support for $command['selector'] in + // the future, once we figure out how to transform a jQuery + // selector to XPath. + $xpath = new DOMXPath($dom); + $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); + if ($wrapperNode) { + // ajax.js adds an enclosing DIV to work around a Safari bug. + $newDom = new DOMDocument(); + $newDom->loadHTML('<div>' . $command['data'] . '</div>'); + $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); + $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; + // The "method" is a jQuery DOM manipulation function. Emulate + // each one using PHP's DOMNode API. + switch ($method) { + case 'replaceWith': + $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); + break; + case 'append': + $wrapperNode->appendChild($newNode); + break; + case 'prepend': + // If no firstChild, insertBefore() falls back to + // appendChild(). + $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); + break; + case 'before': + $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); + break; + case 'after': + // If no nextSibling, insertBefore() falls back to + // appendChild(). + $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); + break; + case 'html': + foreach ($wrapperNode->childNodes as $childNode) { + $wrapperNode->removeChild($childNode); + } + $wrapperNode->appendChild($newNode); + break; } - $wrapperNode->appendChild($newNode); - break; + } } - } + break; + + // @todo Add suitable implementations for these commands in order to + // have full test coverage of what ajax.js can do. + case 'remove': + break; + case 'changed': + break; + case 'css': + break; + case 'data': + break; + case 'restripe': + break; } } - $this->drupalSetContent($dom->saveHTML()); + $content = $dom->saveHTML(); } + $this->drupalSetContent($content); + $this->drupalSetSettings($drupal_settings); return $return; } @@ -2397,6 +2455,13 @@ protected function drupalGetContent() { return $this->content; } + /** + * Gets the value of the Drupal.settings JavaScript variable for the currently loaded page. + */ + protected function drupalGetSettings() { + return $this->drupalSettings; + } + /** * Gets an array containing all e-mails sent during this test case. * @@ -2435,6 +2500,17 @@ protected function drupalSetContent($content, $url = 'internal:') { $this->url = $url; $this->plainTextContent = FALSE; $this->elements = FALSE; + $this->drupalSettings = array(); + if (preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $content, $matches)) { + $this->drupalSettings = drupal_json_decode($matches[1]); + } + } + + /** + * Sets the value of the Drupal.settings JavaScript variable for the currently loaded page. + */ + protected function drupalSetSettings($settings) { + $this->drupalSettings = $settings; } /** diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index 91572bda0d92..9377f4cdbac4 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -269,24 +269,17 @@ class AJAXMultiFormTestCase extends AJAXTestCase { // of field items and "add more" button for the multi-valued field within // each form. $this->drupalGet('form-test/two-instances-of-same-form'); - foreach ($field_xpaths as $form_id => $field_xpath) { + foreach ($field_xpaths as $form_html_id => $field_xpath) { $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == 1, t('Found the correct number of field items on the initial page.')); $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button on the initial page.')); } $this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other'); // Submit the "add more" button of each form twice. After each corresponding - // page update, ensure the same as above. To successfully implement - // consecutive AJAX submissions, we need to manage $settings as ajax.js - // does for Drupal.settings. - preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $this->content, $matches); - $settings = drupal_json_decode($matches[1]); - foreach ($field_xpaths as $form_id => $field_xpath) { + // page update, ensure the same as above. + foreach ($field_xpaths as $form_html_id => $field_xpath) { for ($i=0; $i<2; $i++) { - $button = $this->xpath($field_xpath . $button_xpath_suffix); - $button_id = (string) $button[0]['id']; - $commands = $this->drupalPostAJAX(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_id, $settings['ajax'][$button_id]); - $settings = array_merge_recursive($settings, $commands[0]['settings']); + $this->drupalPostAJAX(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_html_id); $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, t('Found the correct number of field items after an AJAX submission.')); $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button after an AJAX submission.')); $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other'); diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index c3ef7fd9d058..969f3f398f8c 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -996,11 +996,7 @@ class FormsRebuildTestCase extends DrupalWebTestCase { // submission and verify it worked by ensuring the updated page has two text // field items in the field for which we just added an item. $this->drupalGet('node/add/page'); - preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $this->content, $matches); - $settings = drupal_json_decode($matches[1]); - $button = $this->xpath('//input[@name="field_ajax_test_add_more"]'); - $button_id = (string) $button[0]['id']; - $this->drupalPostAJAX(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'page-node-form', $settings['ajax'][$button_id]); + $this->drupalPostAJAX(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'page-node-form'); $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test")]//input[@type="text"]')) == 2, t('AJAX submission succeeded.')); // Submit the form with the non-AJAX "Save" button, leaving the title field diff --git a/modules/simpletest/tests/form_test.file.inc b/modules/simpletest/tests/form_test.file.inc index 96681eaa8086..808863ede11a 100644 --- a/modules/simpletest/tests/form_test.file.inc +++ b/modules/simpletest/tests/form_test.file.inc @@ -13,11 +13,17 @@ function form_test_load_include_menu($form, &$form_state) { // Submit the form via AJAX. That way the FAPI has to care about including // the file specified in hook_menu(). + $ajax_wrapper_id = drupal_html_id('form-test-load-include-menu-ajax-wrapper'); + $form['ajax_wrapper'] = array( + '#markup' => '<div id="' . $ajax_wrapper_id . '"></div>', + ); $form['button'] = array( '#type' => 'submit', '#value' => t('Save'), '#submit' => array('form_test_load_include_submit'), '#ajax' => array( + 'wrapper' => $ajax_wrapper_id, + 'method' => 'append', 'callback' => 'form_test_load_include_menu_ajax', ), ); @@ -32,9 +38,12 @@ function form_test_load_include_submit($form, $form_state) { } /** - * Ajax callback for the file inclusion via menu test. We don't need to return - * anything as the messages are added automatically. + * Ajax callback for the file inclusion via menu test. */ function form_test_load_include_menu_ajax($form) { + // We don't need to return anything, since #ajax['method'] is 'append', which + // does not remove the original #ajax['wrapper'] element, and status messages + // are automatically added by the AJAX framework as long as there's a wrapper + // element to add them to. return ''; } -- GitLab