diff --git includes/form.inc includes/form.inc index 00d9061..5f84c54 100644 --- includes/form.inc +++ includes/form.inc @@ -1042,6 +1042,13 @@ function form_builder($form_id, $element, &$form_state) { else { $form_state['process_input'] = FALSE; } + // In addition to submitted form values, + // _form_builder_handle_input_elements() automatically takes over stored + // form values upon rebuild. If there was a validation error, prevent this + // from happening. + if (form_get_errors()) { + $form_state['values'] = array(); + } } if (!isset($element['#id'])) { @@ -1167,19 +1174,31 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { $value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value'; if ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))) { + // Check for a submitted value. $input = $form_state['input']; foreach ($element['#parents'] as $parent) { $input = isset($input[$parent]) ? $input[$parent] : NULL; } + // Check for a stored value. + $value = $form_state['values']; + foreach ($element['#parents'] as $parent) { + $value = isset($value[$parent]) ? $value[$parent] : NULL; + } // If we have input for the current element, assign it to the #value property. - if (!$form_state['programmed'] || isset($input)) { - // Call #type_value to set the form value; - if (function_exists($value_callback)) { + if (!$form_state['programmed'] || isset($input) || isset($value)) { + // Call the value callback to set the form element value. + if (isset($input) && function_exists($value_callback)) { $element['#value'] = $value_callback($element, $input, $form_state); } + // If we do not have a #value yet, check whether we have fresh input. if (!isset($element['#value']) && isset($input)) { $element['#value'] = $input; } + // If no #value was assigned through submitted values, check whether we + // have a value in $form_state. + if (!isset($element['#value']) && isset($value)) { + $element['#value'] = $value; + } } // Mark all posted values for validation. if (isset($element['#value']) || (!empty($element['#required']))) { diff --git modules/simpletest/drupal_web_test_case.php modules/simpletest/drupal_web_test_case.php index 57e95ed..045c4bb 100644 --- modules/simpletest/drupal_web_test_case.php +++ modules/simpletest/drupal_web_test_case.php @@ -1500,6 +1500,82 @@ class DrupalWebTestCase extends DrupalTestCase { } } + + /** + * Execute a POST request on an AJAX path (normally system/ajax). + * It will be done as usual POST request with SimpleBrowser. + * + * @param $path + * Location of the post form. A Drupal path. + * @param $edit + * Field data in an associative array. Changes the current input fields + * (where possible) to the values indicated. A checkbox can be set to + * TRUE to be checked and FALSE to be unchecked. Note that when a form + * contains file upload fields, other fields cannot start with the '@' + * character. + * + * Multiple select fields can be set using name[] and setting each of the + * possible values. Example: + * $edit = array(); + * $edit['name[]'] = array('value1', 'value2'); + * @param $triggering_element + * Name of the element that triggered the AJAX operation. + * @param $ajax_path + * Path to which to post the AJAX request. + * @param $options + * Options to be forwarded to url(). + * @param $headers + * An array containing additional HTTP request headers, each formatted as + * "name: value". + */ + protected function AJAXPost($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array()) { + if (isset($path)) { + $html = $this->drupalGet($path, $options); + } + + if ($this->parse()) { + $edit_save = $edit; + // Let's iterate over all the forms. + $forms = $this->xpath('//form'); + foreach ($forms as $form) { + // We try to set the fields of this form as specified in $edit. + $edit = $edit_save; + $post = array(); + $upload = array(); + $this->handleForm($post, $edit, $upload, NULL, $form); + $action = $this->getAbsoluteUrl($ajax_path); + $post_array = $post; + foreach ($post as $key => $value) { + // Encode according to application/x-www-form-urlencoded + // Both names and values needs to be urlencoded, according to + // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 + $post[$key] = urlencode($key) . '=' . urlencode($value); + } + $post['ajax_triggering_element'] = 'ajax_triggering_element=' . urlencode($triggering_element); + $post = implode('&', $post); + // debug($post, "POST data"); + $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. + $this->refreshVariables(); + + // Replace original page output with new output from redirected page(s). + if (($new = $this->checkForMetaRefresh())) { + $out = $new; + } + $this->verbose('AJAX POST request to: ' . $ajax_path . + '
Ending URL: ' . $this->getUrl() . + '
Fields: ' . highlight_string('' . $out); + return json_decode($out, TRUE); + + } + // We have not found a form which contained all fields of $edit. + foreach ($edit as $name => $value) { + $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); + } + } + } + /** * Runs cron in the Drupal installed by Simpletest. */ diff --git modules/simpletest/tests/ajax.test modules/simpletest/tests/ajax.test index c70e7e4..64a9702 100644 --- modules/simpletest/tests/ajax.test +++ modules/simpletest/tests/ajax.test @@ -3,7 +3,7 @@ class AJAXTestCase extends DrupalWebTestCase { function setUp() { - parent::setUp('ajax_test'); + parent::setUp('ajax_test', 'ajax_forms_test'); } function drupalGetAJAX($path, $query = array()) { @@ -78,3 +78,51 @@ class AJAXCommandsTestCase extends AJAXTestCase { } } +/** + * Test that $form_state['values'] is properly delivered to $ajax['callback']. + */ +class AJAXFormValuesTestCase extends AJAXTestCase { + + public static function getInfo() { + return array( + 'name' => 'AJAX Form Values in command', + 'description' => 'Creates a form, then POSTS to system/ajax to change the form via pseudo-ajax.', + 'group' => 'AJAX', + ); + } + + function setUp() { + parent::setUp('ajax_forms_test'); + } + + /** + * Create a simple form with a select, then POST a change to it. + */ + function testSimpleAJAXFormValue() { + $form_path = 'ajax_forms_test_get_form'; + $web_user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($web_user); + // $page_data = $this->drupalGet($form_path); + + $edit = array(); + + $selects_to_test = array('red', 'green', 'blue'); + foreach($selects_to_test as $item) { + $edit['select'] = $item; + $commands = $this->AJAXPost($form_path, $edit, 'select'); + $data_command = $commands[2]; + $this->assertTrue($data_command['value'] == $edit['select'], "Expected form_state['values'] == '{$edit['select']}') and received '{$data_command['value']}'"); + } + + $values_to_test = array(FALSE,TRUE); + foreach($values_to_test as $item) { + $edit['checkbox'] = $item; + $commands = $this->AJAXPost($form_path, $edit, 'checkbox'); + $data_command = $commands[2]; + $value_expected = (int)$item; + $value_received = (int)$data_command['value']; + $this->assertTrue($value_received == $value_expected, "Expected form_state['values'] == '$value_expected') and received '{$value_received}'"); + } + } +} + diff --git modules/simpletest/tests/ajax_forms_test.info modules/simpletest/tests/ajax_forms_test.info new file mode 100644 index 0000000..cd02ed4 --- /dev/null +++ modules/simpletest/tests/ajax_forms_test.info @@ -0,0 +1,8 @@ +; $Id: database_test.info,v 1.2 2008/10/09 22:51:40 webchick Exp $ +name = "AJAX form test mock module" +description = "Test for AJAX form calls." +core = 7.x +package = Testing +files[] = ajax_forms_test.module +version = VERSION +hidden = TRUE diff --git modules/simpletest/tests/ajax_forms_test.module modules/simpletest/tests/ajax_forms_test.module new file mode 100644 index 0000000..e8def93 --- /dev/null +++ modules/simpletest/tests/ajax_forms_test.module @@ -0,0 +1,306 @@ + 'AJAX Forms Simple Form Test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ajax_forms_test_simple_form'), + 'access callback' => TRUE, + ); + $items['ajax_forms_test_ajax_commands_form'] = array( + 'title' => 'AJAX Forms AJAX Commands Test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ajax_forms_test_ajax_commands_form'), + 'access callback' => TRUE, + ); + return $items; +} + + +/** + * A basic form used to test form_state['values'] during callback. + * @param $form + * @param $form_state + * @return unknown_type + */ +function ajax_forms_test_simple_form($form, &$form_state) { + $form = array(); + $form['select'] = array( + '#type' => 'select', + '#options' => array('red' => 'red', 'green' => 'green', 'blue' => 'blue'), + '#ajax' => array( + 'callback' => 'ajax_forms_test_simple_form_select_callback', + ), + '#suffix' => "
No color yet selected
", + ); + + $form['checkbox'] = array( + '#type' => 'checkbox', + '#title' => 'Test checkbox', + '#ajax' => array( + 'callback' => 'ajax_forms_test_simple_form_checkbox_callback', + ), + '#suffix' => '
No action yet
', + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'submit', + ); + return $form; +} + +function ajax_forms_test_simple_form_select_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_html('#ajax_selected_color', $form_state['values']['select']); + $commands[] = ajax_command_data('#ajax_selected_color', 'form_state_value_select', $form_state['values']['select']); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + +function ajax_forms_test_simple_form_checkbox_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_html('#ajax_checkbox_value', (int)$form_state['values']['checkbox']); + $commands[] = ajax_command_data('#ajax_checkbox_value', 'form_state_value_select', (int)$form_state['values']['checkbox']); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +/***** --------- For testing CTools-style AJAX Commands --------- *****/ + +/** + * Form to display the AJAX Commands. + * @param $form + * @param $form_state + * @return unknown_type + */ +function ajax_forms_test_ajax_commands_form($form, &$form_state) { + $form = array(); + + // Shows the 'after' command with a callback generating commands. + + $form['after_command_example'] = array( + '#value' => t("AJAX 'After': Click to put something after the div"), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_after_callback', + ), + '#suffix' => "
Something can be inserted after this
+
'After' Command Status: Unknown
" + ); + + // Shows the 'alert' command. + $form['alert_command_example'] = array( + '#value' => t("AJAX 'Alert': Click to alert"), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_alert_callback', + ), + ); + + // Shows the 'append' command. + $form['append_command_example'] = array( + '#value' => t("AJAX 'Append': Click to append something"), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_append_callback', + ), + '#suffix' => "
Append inside this div
", + ); + + + // Shows the 'before' command. + $form['before_command_example'] = array( + '#value' => t("AJAX 'before': Click to put something before the div"), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_before_callback', + ), + '#suffix' => "
Insert something before this.
", + ); + + // Shows the 'changed' command. + $form['changed_command_example'] = array( + '#value' => t("AJAX changed: Click to mark div changed."), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_changed_callback', + ), + '#suffix' => "
This div can be marked as changed or not.
", + ); + + // Shows the AJAX 'css' command. + // Note that this won't work until http://drupal.org/node/623320 lands. + $form['css_command_example'] = array( + '#title' => t("AJAX CSS: Choose the color you'd like the '#box' div to be."), + '#type' => 'select', + '#options' => array('green' => 'green', 'blue' => 'blue'), + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_css_callback', + ), + '#suffix' => "
box
", + ); + + + // Shows the AJAX 'data' command. But there is no use of this information, + // as this would require a javascript client to use the data. + $form['data_command_example'] = array( + '#value' => t("AJAX data command: Issue command."), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_data_callback', + ), + '#suffix' => "
Data attached to this div.
", + ); + + // Shows the AJAX 'html' command. + $form['html_command_example'] = array( + '#value' => t("AJAX html: Replace the HTML in a selector."), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_html_callback', + ), + '#suffix' => "
Original contents
", + ); + + // Shows the AJAX 'prepend' command. + $form['prepend_command_example'] = array( + '#value' => t("AJAX 'prepend': Click to prepend something"), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_prepend_callback', + ), + '#suffix' => "
Something can be prepended to this div...
", + ); + + // Shows the AJAX 'remove' command. + $form['remove_command_example'] = array( + '#value' => t("AJAX 'remove': Click to remove text"), + '#type' => 'submit', + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_remove_callback', + ), + '#suffix' => "
text to be removed
", + ); + + // Show off the AJAX 'restripe' command. + $form['restripe_command_example'] = array( + '#type' => 'submit', + '#value' => t("AJAX 'restripe' command"), + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_restripe_callback', + ), + '#suffix' => "
+ + + +
first row
second row
+
", + + + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} + + +function ajax_forms_test_advanced_commands_after_callback($form, $form_state) { + $selector = '#after_div'; + + $commands = array(); + $commands[] = ajax_command_after($selector, "This will be placed after"); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_alert_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_alert("Alert"); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_append_callback($form, $form_state) { + $selector = '#append_div'; + + $commands = array(); + $commands[] = ajax_command_append($selector, "Appended text"); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_before_callback($form, $form_state) { + $selector = '#before_div'; + + $commands = array(); + $commands[] = ajax_command_before($selector, "Before text"); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + +function ajax_forms_test_advanced_commands_changed_callback($form, $form_state) { + $checkbox_value = $form_state['values']['changed_command_example']; + $checkbox_value_string = $checkbox_value ? "TRUE" : "FALSE"; + $commands = array(); + if ($checkbox_value) { + // This does not yet exercise the 2nd arg (asterisk) so that should be added + // when it works. + $commands[] = ajax_command_changed( '#changed_div'); + } + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_css_callback($form, $form_state) { + $selector = '#css_div'; + $color = $form_state['values']['css_command_example']; + + $commands = array(); + $commands[] = ajax_command_css($selector, array('background-color' => $color)); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_data_callback($form, $form_state) { + $selector = '#data_div'; + + $commands = array(); + $commands[] = ajax_command_data($selector, 'testkey', 'testvalue'); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_html_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_html('#html_div', 'replacement text'); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_prepend_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_prepend('#prepend_div', "prepended text"); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + + +function ajax_forms_test_advanced_commands_remove_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_remove('#remove_text'); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +} + +function ajax_forms_test_advanced_commands_restripe_callback($form, $form_state) { + $commands = array(); + $commands[] = ajax_command_restripe('#restripe_table'); + return array('#type' => 'ajax_commands', '#ajax_commands' => $commands); +}