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