Index: modules/simpletest/tests/common.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v
retrieving revision 1.90
diff -u -r1.90 common.test
--- modules/simpletest/tests/common.test	11 Nov 2009 00:48:56 -0000	1.90
+++ modules/simpletest/tests/common.test	20 Nov 2009 23:41:26 -0000
@@ -1723,3 +1723,55 @@
     $this->assertIdentical(drupal_attributes(array()), '', t('Empty attributes array.'));
   }
 }
+
+/**
+ * Tests converting PHP variables to JSON strings and back.
+ */
+class DrupalJSONTest extends DrupalUnitTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'JSON',
+      'description' => 'Perform unit tests on the drupal_json_encode() and drupal_json_decode() functions.',
+      'group' => 'System',
+    );
+  }
+
+  /**
+   * Tests converting PHP variables to JSON strings and back.
+   */
+  function testJSON() {
+    // Setup a string with the full ASCII table.
+    // @todo: Add tests for non-ASCII characters and Unicode.
+    $str = '';
+    for ($i=0; $i < 128; $i++) {
+      $str .= chr($i);
+    }
+    // Characters that must be escaped.
+    $html_unsafe = array('<', '>', '&');
+
+    // Verify there aren't character encoding problems with the source string.
+    $this->assertIdentical(strlen($str), 128, t('A string with the full ASCII table has the correct length.'));
+    foreach ($html_unsafe as $char) {
+      $this->assertTrue(strpos($str, $char) > 0, t('A string with the full ASCII table includes @s.', array('@s' => $char)));
+    }
+
+    // Verify that JSON encoding produces a string with all of the characters.
+    $json = drupal_json_encode($str);
+    $this->assertTrue(strlen($json) > strlen($str), t('A JSON encoded string is larger than the source string.'));
+
+    // Verify that encoding/decoding is reversible.
+    $json_decoded = drupal_json_decode($json);
+    $this->assertIdentical($str, $json_decoded, t('Encoding a string to JSON and decoding back results in the original string.'));
+
+    // Verify reversibility for structured data. Also verify that necessary
+    // characters are escaped.
+    $source = array(TRUE, FALSE, 0, 1, '0', '1', $str, array('key1' => $str, 'key2' => array('nested' => TRUE)));
+    $json = drupal_json_encode($source);
+    foreach ($html_unsafe as $char) {
+      $this->assertTrue(strpos($json, $char) === FALSE, t('A JSON encoded string does not contain @s.', array('@s' => $char)));
+    }
+    $json_decoded = drupal_json_decode($json);
+    $this->assertNotIdentical($source, $json, t('An array encoded in JSON is not identical to the source.'));
+    $this->assertIdentical($source, $json_decoded, t('Encoding structured data to JSON and decoding back results in the original data.'));
+  }
+}
Index: modules/poll/poll.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v
retrieving revision 1.25
diff -u -r1.25 poll.test
--- modules/poll/poll.test	16 Oct 2009 23:48:37 -0000	1.25
+++ modules/poll/poll.test	20 Nov 2009 23:41:24 -0000
@@ -339,30 +339,11 @@
       'choice[new:1][chtext]' => $this->randomName(),
     );
 
-    // @TODO: the framework should make it possible to submit a form to a
-    // different URL than its action or the current. For now, we can just force
-    // it.
-    $this->additionalCurlOptions[CURLOPT_URL] = url('system/ajax', array('absolute' => TRUE));
-    $this->drupalPost(NULL, $edit, t('More choices'));
-    unset($this->additionalCurlOptions[CURLOPT_URL]);
+    // Press 'add choice' button through AJAX, and place the expected HTML result
+    // as the tested content.
+    $commands = $this->drupalPostAJAX(NULL, $edit, 'poll_more');
+    $this->content = $commands[1]['data'];
 
-    // The response is drupal_json_output, so we need to undo some escaping.
-    $commands = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent()));
-
-    // The JSON response will be two AJAX commands. The first is a settings
-    // command and the second is the replace command.
-    $settings = reset($commands);
-    $replace = next($commands);
-
-    $this->assertTrue(is_object($settings), t('The response settings command is an object'));
-    $this->assertTrue(is_object($replace), t('The response replace command is an object'));
-
-    // This replace data is valid HTML so we will can reuse everything we have
-    // for HTML pages.
-    $this->content = $replace->data;
-
-    // Needs to be emptied out so the new content will be parsed.
-    $this->elements = '';
     $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0)));
     $this->assertFieldByName('choice[chid:1][chtext]', $edit['choice[new:1][chtext]'], t('Field !i found', array('!i' => 1)));
     $this->assertFieldByName('choice[new:0][chtext]', '', t('Field !i found', array('!i' => 2)));
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.172
diff -u -r1.172 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	20 Nov 2009 05:14:13 -0000	1.172
+++ modules/simpletest/drupal_web_test_case.php	20 Nov 2009 23:41:25 -0000
@@ -1404,8 +1404,7 @@
    * Retrieve a Drupal path or an absolute path and JSON decode the result.
    */
   function drupalGetAJAX($path, array $options = array(), array $headers = array()) {
-    $out = $this->drupalGet($path, $options, $headers);
-    return json_decode($out, TRUE);
+    return drupal_json_decode($this->drupalGet($path, $options, $headers));
   }
 
   /**
@@ -1545,8 +1544,7 @@
    * Execute a POST request on an AJAX path and JSON decode the result.
    */
   protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array()) {
-    $out = $this->drupalPost($path, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers);
-    return json_decode($out, TRUE);
+    return drupal_json_decode($this->drupalPost($path, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers));
   }
 
   /**
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1050
diff -u -r1.1050 common.inc
--- includes/common.inc	20 Nov 2009 06:12:45 -0000	1.1050
+++ includes/common.inc	20 Nov 2009 23:41:22 -0000
@@ -4342,10 +4342,22 @@
  * Converts a PHP variable into its Javascript equivalent.
  *
  * We use HTML-safe strings, i.e. with <, > and & escaped.
+ *
+ * @see drupal_json_decode()
  */
 function drupal_json_encode($var) {
-  // json_encode() does not escape <, > and &, so we do it with str_replace()
-  return str_replace(array("<", ">", "&"), array('\x3c', '\x3e', '\x26'), json_encode($var));
+  // json_encode() does not escape <, > and &, so we do it with str_replace().
+  return str_replace(array('<', '>', '&'), array('\x3c', '\x3e', '\x26'), json_encode($var));
+}
+
+/**
+ * Converts an HTML-safe JSON string into its PHP equivalent.
+ *
+ * @see drupal_json_encode()
+ */
+function drupal_json_decode($var) {
+  // json_decode() does not unescape <, > and &, so we do it with str_replace().
+  return json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array('<', '>', '&'), $var), TRUE);
 }
 
 /**
Index: modules/field/tests/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v
retrieving revision 1.1
diff -u -r1.1 field.test
--- modules/field/tests/field.test	20 Nov 2009 23:29:28 -0000	1.1
+++ modules/field/tests/field.test	20 Nov 2009 23:41:24 -0000
@@ -1552,8 +1552,8 @@
     $this->drupalGet('test-entity/add/test-bundle');
 
     // Press 'add more' button a couple times -> 3 widgets.
-    // The drupalPostAhah() helper will not work iteratively, so we add those.
-    // through non-'JS' submission.
+    // drupalPostAJAX() will not work iteratively, so we add those through
+    // non-JS submission.
     $this->drupalPost(NULL, array(), t('Add another item'));
     $this->drupalPost(NULL, array(), t('Add another item'));
 
@@ -1575,8 +1575,10 @@
       $field_values[$weight]['value'] = (string)$value;
       $pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*";
     }
-    // Press 'add more' button through AHAH.
-    $this->_fieldPostAhah($edit, t('Add another item'));
+    // Press 'add more' button through AJAX, and place the expected HTML result
+    // as the tested content.
+    $commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more');
+    $this->content = $commands[1]['data'];
 
     ksort($values);
     $values = array_values($values);
@@ -1650,39 +1652,6 @@
     $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, t('New revision has the expected value for the field with no edit access.'));
     $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('New revision has the expected value for the field with edit access.'));
   }
-
-  /**
-   * Execute a POST request on a AHAH callback.
-   *
-   * Stolen from poll.test. The JSON result is parsed into HTML and placed in
-   * $this->content, so that regular asserts can be performed.
-   *
-   * Since the result is generally not a full-fledged form, this cannot be
-   * called iteratively.
-   */
-  function _fieldPostAhah($edit, $submit, array $options = array(), array $headers = array()) {
-    $this->additionalCurlOptions[CURLOPT_URL] = url('system/ajax', array('absolute' => TRUE));
-    $this->drupalPost(NULL, $edit, $submit);
-    unset($this->additionalCurlOptions[CURLOPT_URL]);
-
-    // The response is drupal_json_output, so we need to undo some escaping.
-    $commands = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent()));
-
-    // The JSON response will be two AJAX commands. The first is a settings
-    // command and the second is the replace command.
-    $settings = reset($commands);
-    $replace = next($commands);
-
-    $this->assertTrue(is_object($settings), t('The response settings command is an object'));
-    $this->assertTrue(is_object($replace), t('The response replace command is an object'));
-
-    // This response data is valid HTML so we will can reuse everything we have
-    // for HTML pages.
-    $this->content = $replace->data;
-
-    // Needs to be emptied out so the new content will be parsed.
-    $this->elements = '';
-  }
 }
 
 class FieldCrudTestCase extends FieldTestCase {
