Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1141
diff -u -p -r1.1141 common.inc
--- includes/common.inc	6 Apr 2010 06:43:19 -0000	1.1141
+++ includes/common.inc	6 Apr 2010 17:10:46 -0000
@@ -3321,13 +3321,73 @@ function drupal_html_class($class) {
 /**
  * Prepare a string for use as a valid HTML ID and guarantee uniqueness.
  *
+ * This function ensures that each passed HTML ID value only exists once on the
+ * page. By tracking the already returned ids, this function enables forms,
+ * blocks, and other content to be output multiple times on the same page,
+ * without breaking (X)HTML validation.
+ *
+ * For already existing ids, a counter is appended to the id string. Therefore,
+ * JavaScript and CSS code should not rely on any value that was generated by
+ * this function and instead should rely on manually added CSS classes or
+ * similarly reliable constructs.
+ *
+ * Two consecutive hyphens separate the counter from the original id. To manage
+ * uniqueness across multiple AJAX requests on the same page, AJAX requests
+ * POST an array of all IDs currently present on the page, which are used to
+ * prime this function's cache upon first invocation.
+ *
+ * To allow reverse-parsing of ids submitted via AJAX, any multiple consecutive
+ * hyphens in the originally passed $id are replaced with a single hyphen.
+ *
  * @param $id
  *   The ID to clean.
+ *
  * @return
  *   The cleaned ID.
  */
 function drupal_html_id($id) {
-  $seen_ids = &drupal_static(__FUNCTION__, array());
+  // If this is an AJAX request, then HTML ids must not only be unique across
+  // this page request, but also not duplicate what is already on the base page.
+  // Therefore, initialize $seen_ids to take into account ids that are already
+  // in use on the base page.
+  $seen_ids_init = &drupal_static(__FUNCTION__ . ':init');
+  if (!isset($seen_ids_init)) {
+    // Ideally, Drupal would provide an API to persist state information about
+    // prior page requests in the database, and we'd be able to add this
+    // function's $seen_ids static variable to that state information in order
+    // to have it properly initialized for this page request. However, no such
+    // page state API exists, so instead, ajax.js adds all of the in-use HTML
+    // ids to the POST data of AJAX submissions. Direct use of $_POST is
+    // normally not recommended as it could open up security risks, but because
+    // the raw POST data is cast to a number before being returned by this
+    // function, this usage is safe.
+    if (empty($_POST['ajax_html_ids'])) {
+      $seen_ids_init = array();
+    }
+    else {
+      // This function ensures uniqueness by appending a counter to the base id
+      // requested by the calling function after the first occurrence of that
+      // requested id. $_POST['ajax_html_ids'] contains the ids as they were
+      // returned by this function, potentially with the appended counter, so
+      // we parse that to reconstruct the $seen_ids array.
+      foreach ($_POST['ajax_html_ids'] as $seen_id) {
+        // We rely on '--' being used solely for separating a base id from the
+        // counter, which this function ensures when returning an id.
+        $parts = explode('--', $seen_id, 2);
+        if (!empty($parts[1]) && is_numeric($parts[1])) {
+          list($seen_id, $i) = $parts;
+        }
+        else {
+          $i = 1;
+        }
+        if (!isset($seen_ids_init[$seen_id]) || ($i > $seen_ids_init[$seen_id])) {
+          $seen_ids_init[$seen_id] = $i;
+        }
+      }
+    }
+  }
+  $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init);
+
   $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
 
   // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
@@ -3338,14 +3398,15 @@ function drupal_html_id($id) {
   // characters as well.
   $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
 
-  // Ensure IDs are unique. The first occurrence is held but left alone.
-  // Subsequent occurrences get a number appended to them. This incrementing
-  // will almost certainly break code that relies on explicit HTML IDs in forms
-  // that appear more than once on the page, but the alternative is outputting
-  // duplicate IDs, which would break JS code and XHTML validity anyways. For
-  // now, it's an acceptable stopgap solution.
+  // Removing multiple consecutive hyphens.
+  $id = preg_replace('/\-+/', '-', $id);
+  // Ensure IDs are unique by appending a counter after the first occurrence.
+  // The counter needs to be appended with a delimiter that does not exist in
+  // the base ID. Requiring a unique delimiter helps ensure that we really do
+  // return unique IDs and also helps us re-create the $seen_ids array during
+  // AJAX requests.
   if (isset($seen_ids[$id])) {
-    $id = $id . '-' . ++$seen_ids[$id];
+    $id = $id . '--' . ++$seen_ids[$id];
   }
   else {
     $seen_ids[$id] = 1;
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.14
diff -u -p -r1.14 ajax.js
--- misc/ajax.js	1 Apr 2010 12:23:41 -0000	1.14
+++ misc/ajax.js	6 Apr 2010 17:10:47 -0000
@@ -200,6 +200,12 @@ Drupal.ajax.prototype.beforeSubmit = fun
   // Disable the element that received the change.
   $(this.element).addClass('progress-disabled').attr('disabled', true);
 
+  // Prevent duplicate HTML ids in the returned markup.
+  // @see drupal_html_id()
+  $('[id]').each(function () {
+    form_values.push({ name: 'ajax_html_ids[]', value: this.id });
+  });
+
   // Insert progressbar or throbber.
   if (this.progress.type == 'bar') {
     var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.45
diff -u -p -r1.45 field.form.inc
--- modules/field/field.form.inc	27 Mar 2010 05:52:49 -0000	1.45
+++ modules/field/field.form.inc	6 Apr 2010 17:10:48 -0000
@@ -153,7 +153,7 @@ function field_multiple_value_form($fiel
 
   $title = check_plain(t($instance['label']));
   $description = field_filter_xss(t($instance['description']));
-  $wrapper_id = drupal_html_class($field_name) . '-add-more-wrapper';
+  $wrapper_id = drupal_html_id($field_name . '-add-more-wrapper');
   $field_elements = array();
 
   $function = $instance['widget']['module'] . '_field_widget_form';
@@ -241,7 +241,7 @@ function theme_field_multiple_value_form
   $output = '';
 
   if ($element['#cardinality'] > 1 || $element['#cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
-    $table_id = $element['#field_name'] . '_values';
+    $table_id = drupal_html_id($element['#field_name'] . '_values');
     $order_class = $element['#field_name'] . '-delta-order';
     $required = !empty($element['#required']) ? '<span class="form-required" title="' . t('This field is required. ') . '">*</span>' : '';
 
Index: modules/field/modules/options/options.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/modules/options/options.test,v
retrieving revision 1.12
diff -u -p -r1.12 options.test
--- modules/field/modules/options/options.test	31 Mar 2010 20:05:06 -0000	1.12
+++ modules/field/modules/options/options.test	6 Apr 2010 17:10:48 -0000
@@ -135,7 +135,7 @@ class OptionsWidgetsTestCase extends Fie
 
     // Display form: with no field data, nothing is checked.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertNoFieldChecked("edit-card-2-$langcode--0");
+    $this->assertNoFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertNoFieldChecked("edit-card-2-$langcode-2");
     $this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', t('Option text was properly filtered.'));
@@ -151,7 +151,7 @@ class OptionsWidgetsTestCase extends Fie
 
     // Display form: check that the right options are selected.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFieldChecked("edit-card-2-$langcode--0");
+    $this->assertFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertFieldChecked("edit-card-2-$langcode-2");
 
@@ -166,7 +166,7 @@ class OptionsWidgetsTestCase extends Fie
 
     // Display form: check that the right options are selected.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFieldChecked("edit-card-2-$langcode--0");
+    $this->assertFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertNoFieldChecked("edit-card-2-$langcode-2");
 
Index: modules/field_ui/field_ui.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field_ui/field_ui.test,v
retrieving revision 1.14
diff -u -p -r1.14 field_ui.test
--- modules/field_ui/field_ui.test	27 Mar 2010 05:52:49 -0000	1.14
+++ modules/field_ui/field_ui.test	6 Apr 2010 17:10:48 -0000
@@ -94,7 +94,7 @@ class FieldUITestCase extends DrupalWebT
     // should also appear in the 'taxonomy term' entity.
     $vocabulary = taxonomy_vocabulary_load(1);
     $this->drupalGet('admin/structure/taxonomy/' . $vocabulary->machine_name . '/fields');
-    $this->assertTrue($this->xpath('//select[@id="edit--add-existing-field-field-name"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.'));
+    $this->assertTrue($this->xpath('//select[@name="_add_existing_field[field_name]"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.'));
   }
 
   /**
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.207
diff -u -p -r1.207 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	31 Mar 2010 20:05:06 -0000	1.207
+++ modules/simpletest/drupal_web_test_case.php	6 Apr 2010 17:10:50 -0000
@@ -1659,7 +1659,9 @@ class DrupalWebTestCase extends DrupalTe
               // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
               $post[$key] = urlencode($key) . '=' . urlencode($value);
             }
-            if ($ajax && isset($submit['triggering_element'])) {
+            // 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']));
@@ -1668,6 +1670,10 @@ class DrupalWebTestCase extends DrupalTe
               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);
           }
@@ -1698,10 +1704,78 @@ class DrupalWebTestCase extends DrupalTe
   }
 
   /**
-   * 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()) {
-    return drupal_json_decode($this->drupalPost($path, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers));
+   * Execute an AJAX submission.
+   *
+   * This executes a POST as ajax.js does. It uses the returned JSON data, an
+   * array of commands, to update $this->content using equivalent DOM
+   * manipulation as is used by ajax.js. It also returns the array of commands.
+   *
+   * @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()) {
+    // 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->content;
+    $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id));
+
+    // We need $ajax_settings['wrapper'] to perform DOM manipulation.
+    if (!empty($ajax_settings) && !empty($return)) {
+      // 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 = DOMDocument::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;
+            }
+          }
+        }
+      }
+      $this->drupalSetContent($dom->saveHTML());
+    }
+    return $return;
   }
 
   /**
@@ -2788,6 +2862,36 @@ class DrupalWebTestCase extends DrupalTe
   }
 
   /**
+   * Assert that each HTML ID is used for just a single element.
+   *
+   * @param $message
+   *   Message to display.
+   * @param $group
+   *   The group this message belongs to.
+   * @param $ids_to_skip
+   *   An optional array of ids to skip when checking for duplicates. It is
+   *   always a bug to have duplicate HTML IDs, so this parameter is to enable
+   *   incremental fixing of core code. Whenever a test passes this parameter,
+   *   it should add a "todo" comment above the call to this function explaining
+   *   the legacy bug that the test wishes to ignore and including a link to an
+   *   issue that is working to fix that legacy bug.
+   * @return
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertNoDuplicateIds($message = '', $group = 'Other', $ids_to_skip = array()) {
+    $status = TRUE;
+    foreach ($this->xpath('//*[@id]') as $element) {
+      $id = (string) $element['id'];
+      if (isset($seen_ids[$id]) && !in_array($id, $ids_to_skip)) {
+        $this->fail(t('The HTML ID %id is unique.', array('%id' => $id)), $group);
+        $status = FALSE;
+      }
+      $seen_ids[$id] = TRUE;
+    }
+    return $this->assert($status, $message, $group);
+  }
+
+  /**
    * Helper function: construct an XPath for the given set of attributes and value.
    *
    * @param $attribute
Index: modules/simpletest/tests/ajax.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v
retrieving revision 1.7
diff -u -p -r1.7 ajax.test
--- modules/simpletest/tests/ajax.test	26 Mar 2010 18:58:12 -0000	1.7
+++ modules/simpletest/tests/ajax.test	6 Apr 2010 17:10:50 -0000
@@ -2,8 +2,8 @@
 // $Id: ajax.test,v 1.7 2010/03/26 18:58:12 dries Exp $
 
 class AJAXTestCase extends DrupalWebTestCase {
-  function setUp() {
-    parent::setUp('ajax_test', 'ajax_forms_test');
+  function setUp($modules = array()) {
+    parent::setUp(array_unique(array_merge(array('ajax_test', 'ajax_forms_test'), $modules)));
   }
 
   /**
@@ -204,6 +204,91 @@ class AJAXFormValuesTestCase extends AJA
   }
 }
 
+/**
+ * Tests that AJAX-enabled forms work when multiple instances of the same form are on a page.
+ */
+class AJAXMultiFormTestCase extends AJAXTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'AJAX multi form',
+      'description' => 'Tests that AJAX-enabled forms work when multiple instances of the same form are on a page.',
+      'group' => 'AJAX',
+    );
+  }
+
+  function setUp() {
+    parent::setUp(array('form_test'));
+
+    // Create a multi-valued field for 'page' nodes to use for AJAX testing.
+    $field_name = 'field_ajax_test';
+    $field = array(
+      'field_name' => $field_name,
+      'type' => 'text',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+    );
+    field_create_field($field);
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    );
+    field_create_instance($instance);
+
+    // Login a user who can create 'page' nodes.
+    $this->web_user = $this->drupalCreateUser(array('create page content'));
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Test that a page with the 'page_node_form' included twice works correctly.
+   */
+  function testMultiForm() {
+    // HTML IDs for elements within the field are potentially modified with
+    // each AJAX submission, but these variables are stable and help target the
+    // desired elements.
+    $field_name = 'field_ajax_test';
+    $field_xpaths = array(
+      'page-node-form' => '//form[@id="page-node-form"]//div[contains(@class, "field-name-field-ajax-test")]',
+      'page-node-form--2' => '//form[@id="page-node-form--2"]//div[contains(@class, "field-name-field-ajax-test")]',
+    );
+    $button_name = $field_name . '_add_more';
+    $button_value = t('Add another item');
+    $button_xpath_suffix = '//input[@name="' . $button_name . '"]';
+    $field_items_xpath_suffix = '//input[@type="text"]';
+
+    // Ensure the initial page contains both node forms and the correct number
+    // 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) {
+      $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.'));
+    }
+    // @todo Legacy bug of duplicate ids for filter-guidelines-FORMAT. See
+    //   http://drupal.org/node/755566.
+    $this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other', array('filter-guidelines-1', 'filter-guidelines-2', 'filter-guidelines-3'));
+
+    // 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) {
+      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->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.'));
+        // @todo Legacy bug of duplicate ids for filter-guidelines-FORMAT. See
+        //   http://drupal.org/node/755566.
+        $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other', array('filter-guidelines-1', 'filter-guidelines-2', 'filter-guidelines-3'));
+      }
+    }
+  }
+}
 
 /**
  * Miscellaneous AJAX tests using ajax_test module.
Index: modules/simpletest/tests/common.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v
retrieving revision 1.107
diff -u -p -r1.107 common.test
--- modules/simpletest/tests/common.test	6 Apr 2010 15:01:27 -0000	1.107
+++ modules/simpletest/tests/common.test	6 Apr 2010 17:10:50 -0000
@@ -748,15 +748,15 @@ class DrupalHTMLIdentifierTestCase exten
     $this->assertIdentical(drupal_html_id('invalid,./:@\\^`{Üidentifier'), 'invalididentifier', t('Strip invalid characters.'));
 
     // Verify Drupal coding standards are enforced.
-    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name--1', t('Enforce Drupal coding standards.'));
+    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name-1', t('Enforce Drupal coding standards.'));
 
     // Reset the static cache so we can ensure the unique id count is at zero.
     drupal_static_reset('drupal_html_id');
 
     // Clean up IDs with invalid starting characters.
     $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id', t('Test the uniqueness of IDs #1.'));
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id-2', t('Test the uniqueness of IDs #2.'));
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id-3', t('Test the uniqueness of IDs #3.'));
+    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--2', t('Test the uniqueness of IDs #2.'));
+    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--3', t('Test the uniqueness of IDs #3.'));
   }
 }
 
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.33
diff -u -p -r1.33 form_test.module
--- modules/simpletest/tests/form_test.module	26 Mar 2010 18:58:12 -0000	1.33
+++ modules/simpletest/tests/form_test.module	6 Apr 2010 17:10:51 -0000
@@ -126,6 +126,18 @@ function form_test_menu() {
     'type' => MENU_CALLBACK,
   );
 
+  if (module_exists('node')) {
+    $items['form-test/two-instances-of-same-form'] = array(
+      'title' => 'AJAX test with two form instances',
+      'page callback' => 'form_test_two_instances',
+      'access callback' => 'node_access',
+      'access arguments' => array('create', 'page'),
+      'file path' => drupal_get_path('module', 'node'),
+      'file' => 'node.pages.inc',
+      'type' => MENU_CALLBACK,
+    );
+  }
+
   return $items;
 }
 
@@ -1035,3 +1047,20 @@ function form_test_user_register_form_re
   drupal_set_message('Form rebuilt.');
   $form_state['rebuild'] = TRUE;
 }
+
+/**
+ * Menu callback that returns two instances of the node form.
+ */
+function form_test_two_instances() {
+  global $user;
+  $node1 = (object) array(
+    'uid' => $user->uid,
+    'name' => (isset($user->name) ? $user->name : ''),
+    'type' => 'page',
+    'language' => LANGUAGE_NONE,
+  );
+  $node2 = clone($node1);
+  $return['node_form_1'] = drupal_get_form('page_node_form', $node1);
+  $return['node_form_2'] = drupal_get_form('page_node_form', $node2);
+  return $return;
+}
