Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.33 diff -u -p -r1.33 ajax.inc --- includes/ajax.inc 27 Sep 2010 00:53:55 -0000 1.33 +++ includes/ajax.inc 30 Sep 2010 23:16:14 -0000 @@ -213,11 +213,66 @@ * functions. */ function ajax_render($commands = array()) { - // Automatically extract any 'settings' added via drupal_add_js() and make - // them the first command. - $scripts = drupal_add_js(NULL, NULL); + // AJAX responses aren't rendered with html.tpl.php, so we have to call + // drupal_get_css() and drupal_get_js() here, in order to have new files added + // during this request to be loaded by the page. We only want to send back + // files that the page hasn't already loaded, so we implement simple diffing + // logic using array_diff_key(). + foreach (array('css', 'js') as $type) { + // It is highly suspicious if $_POST['ajax_page_state'][$type] is empty, + // since the base page ought to have at least one JS file and one CSS file + // loaded. It probably indicates an error, and rather than making the page + // reload all of the files, instead we return no new files. + if (empty($_POST['ajax_page_state'][$type])) { + $items[$type] = array(); + } + else { + $function = 'drupal_add_' . $type; + $items[$type] = $function(); + drupal_alter($type, $items[$type]); + // @todo Inline CSS and JS items are indexed numerically. These can't be + // reliably diffed with array_diff_key(), since the number can change + // due to factors unrelated to the inline content, so for now, we strip + // the inline items from AJAX responses, and can add support for them + // when drupal_add_css() and drupal_add_js() are changed to using md5() + // or some other hash of the inline content. + foreach ($items[$type] as $key => $item) { + if (is_numeric($key)) { + unset($items[$type][$key]); + } + } + // Ensure that the page doesn't reload what it already has. + $items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]); + } + } + + // Render the HTML to load these files, and add AJAX commands to insert this + // HTML in the page. We pass TRUE as second argument to prevent the data from + // being altered again, as we already altered it above. + $styles = drupal_get_css($items['css'], TRUE); + $scripts_footer = drupal_get_js('footer', $items['js'], TRUE); + $scripts_header = drupal_get_js('header', $items['js'], TRUE); + + $extra_commands = array(); + if (!empty($styles)) { + $extra_commands[] = ajax_command_prepend('head', $styles); + } + if (!empty($scripts_header)) { + $extra_commands[] = ajax_command_prepend('head', $scripts_header); + } + if (!empty($scripts_footer)) { + $extra_commands[] = ajax_command_append('body', $scripts_footer); + } + if (!empty($extra_commands)) { + $commands = array_merge($extra_commands, $commands); + } + + $scripts = drupal_add_js(); if (!empty($scripts['settings'])) { - array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $scripts['settings']['data']))); + $settings = $scripts['settings']; + // Automatically extract any settings added via drupal_add_js() and make + // them the first command. + array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $settings['data']), TRUE)); } // Allow modules to alter any AJAX response. @@ -307,6 +362,31 @@ function ajax_form_callback() { } /** + * Theme callback for AJAX requests. + * + * Many different pages can invoke an AJAX request to system/ajax or another + * generic AJAX path. It is almost always desired for an AJAX response to be + * rendered using the same theme as the base page, and it is therefore + * recommended for all AJAX paths to list this function for its 'theme + * callback', as is done in system_menu() for the 'system/ajax' path. + */ +function ajax_base_page_theme() { + if (!empty($_POST['ajax_page_state']['theme']) && !empty($_POST['ajax_page_state']['theme_token'])) { + $theme = $_POST['ajax_page_state']['theme']; + $token = $_POST['ajax_page_state']['theme_token']; + + // Prevent a request forgery from giving a person access to a theme they + // shouldn't be otherwise allowed to see. However, since everyone is allowed + // to see the default theme, token validation isn't required for that, and + // bypassing it allows most use-cases to work even when accessed from the + // page cache. + if ($theme === variable_get('theme_default', 'bartik') || drupal_valid_token($token, $theme)) { + return $theme; + } + } +} + +/** * Package and send the result of a page callback to the browser as an AJAX response. * * @param $page_callback_result Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1228 diff -u -p -r1.1228 common.inc --- includes/common.inc 26 Sep 2010 23:31:35 -0000 1.1228 +++ includes/common.inc 30 Sep 2010 23:16:19 -0000 @@ -2822,16 +2822,22 @@ function drupal_add_css($data = NULL, $o * @param $css * (optional) An array of CSS files. If no array is provided, the default * stylesheets array is used instead. + * @param $skip_alter + * (optional) If set to TRUE, this function skips calling drupal_alter() on + * $css, useful when the calling function passes a $css array that has already + * been altered. * @return * A string of XHTML CSS tags. */ -function drupal_get_css($css = NULL) { +function drupal_get_css($css = NULL, $skip_alter = FALSE) { if (!isset($css)) { $css = drupal_add_css(); } // Allow modules and themes to alter the CSS items. - drupal_alter('css', $css); + if (!$skip_alter) { + drupal_alter('css', $css); + } // Sort CSS items according to their weights. uasort($css, 'drupal_sort_weight'); @@ -2855,6 +2861,12 @@ function drupal_get_css($css = NULL) { '#type' => 'styles', '#items' => $css, ); + + // Provide the page with information about the individual CSS files used, + // information not otherwise available when CSS aggregation is enabled. + $setting['ajaxPageState']['css'] = array_fill_keys(array_keys($css), 1); + $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); + return drupal_render($styles); } @@ -3791,13 +3803,17 @@ function drupal_js_defaults($data = NULL * @param $javascript * (optional) An array with all JavaScript code. Defaults to the default * JavaScript array for the given scope. + * @param $skip_alter + * (optional) If set to TRUE, this function skips calling drupal_alter() on + * $javascript, useful when the calling function passes a $javascript array + * that has already been altered. * @return * All JavaScript code segments and includes for the scope as HTML tags. * @see drupal_add_js() * @see locale_js_alter() * @see drupal_js_defaults() */ -function drupal_get_js($scope = 'header', $javascript = NULL) { +function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALSE) { if (!isset($javascript)) { $javascript = drupal_add_js(); } @@ -3806,13 +3822,15 @@ function drupal_get_js($scope = 'header' } // Allow modules to alter the JavaScript. - drupal_alter('js', $javascript); + if (!$skip_alter) { + drupal_alter('js', $javascript); + } // Filter out elements of the given scope. $items = array(); - foreach ($javascript as $item) { + foreach ($javascript as $key => $item) { if ($item['scope'] == $scope) { - $items[] = $item; + $items[$key] = $item; } } @@ -3845,6 +3863,22 @@ function drupal_get_js($scope = 'header' // Sort the JavaScript by weight so that it appears in the correct order. uasort($items, 'drupal_sort_weight'); + // Provide the page with information about the individual JavaScript files + // used, information not otherwise available when aggregation is enabled. + $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1); + unset($setting['ajaxPageState']['js']['settings']); + drupal_add_js($setting, 'setting'); + + // If we're outputting the header scope, then this might be the final time + // that drupal_get_js() is running, so add the setting to this output as well + // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's + // because drupal_get_js() was intentionally passed a $javascript argument + // stripped off settings, potentially in order to override how settings get + // output, so in this case, do not add the setting to this output. + if ($scope == 'header' && isset($items['settings'])) { + $items['settings']['data'][] = $setting; + } + // Loop through the JavaScript to construct the rendered output. $element = array( '#tag' => 'script', Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.615 diff -u -p -r1.615 theme.inc --- includes/theme.inc 26 Sep 2010 23:31:35 -0000 1.615 +++ includes/theme.inc 30 Sep 2010 23:16:21 -0000 @@ -102,6 +102,15 @@ function drupal_theme_initialize() { // Themes can have alter functions, so reset the drupal_alter() cache. drupal_static_reset('drupal_alter'); + + // Provide the page with information about the theme that's used, so that a + // later AJAX request can be rendered using the same theme. + // @see ajax_base_page_theme() + $setting['ajaxPageState'] = array( + 'theme' => $theme_key, + 'themeToken' => drupal_get_token($theme_key), + ); + drupal_add_js($setting, 'setting'); } /** Index: misc/ajax.js =================================================================== RCS file: /cvs/drupal/drupal/misc/ajax.js,v retrieving revision 1.19 diff -u -p -r1.19 ajax.js --- misc/ajax.js 22 Sep 2010 21:01:39 -0000 1.19 +++ misc/ajax.js 30 Sep 2010 23:16:21 -0000 @@ -231,6 +231,17 @@ Drupal.ajax.prototype.beforeSubmit = fun form_values.push({ name: 'ajax_html_ids[]', value: this.id }); }); + // Allow Drupal to return new JavaScript and CSS files to load without + // returning the ones already loaded. + form_values.push({ name: 'ajax_page_state[theme]', value: Drupal.settings.ajaxPageState.theme }); + form_values.push({ name: 'ajax_page_state[theme_token]', value: Drupal.settings.ajaxPageState.themeToken }); + for (var key in Drupal.settings.ajaxPageState.css) { + form_values.push({ name: 'ajax_page_state[css][' + key + ']', value: 1 }); + } + for (var key in Drupal.settings.ajaxPageState.js) { + form_values.push({ name: 'ajax_page_state[js][' + key + ']', value: 1 }); + } + // 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/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.40 diff -u -p -r1.40 file.module --- modules/file/file.module 13 Sep 2010 01:09:25 -0000 1.40 +++ modules/file/file.module 30 Sep 2010 23:16:22 -0000 @@ -41,12 +41,14 @@ function file_menu() { 'page callback' => 'file_ajax_upload', 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access content'), + 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, ); $items['file/progress'] = array( 'page callback' => 'file_ajax_progress', 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access content'), + 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, ); Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.238 diff -u -p -r1.238 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 28 Sep 2010 03:30:37 -0000 1.238 +++ modules/simpletest/drupal_web_test_case.php 30 Sep 2010 23:16:25 -0000 @@ -2816,7 +2816,7 @@ class DrupalWebTestCase extends DrupalTe */ protected function assertFieldByXPath($xpath, $value, $message = '', $group = 'Other') { $fields = $this->xpath($xpath); - + debug($fields); // If value specified then check array for match. $found = TRUE; if ($value) { Index: modules/simpletest/tests/ajax.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v retrieving revision 1.18 diff -u -p -r1.18 ajax.test --- modules/simpletest/tests/ajax.test 28 Sep 2010 02:30:32 -0000 1.18 +++ modules/simpletest/tests/ajax.test 30 Sep 2010 23:16:25 -0000 @@ -2,27 +2,52 @@ // $Id: ajax.test,v 1.18 2010/09/28 02:30:32 dries Exp $ class AJAXTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + function setUp() { $modules = func_get_args(); if (isset($modules[0]) && is_array($modules[0])) { $modules = $modules[0]; } - parent::setUp(array_unique(array_merge(array('ajax_test', 'ajax_forms_test'), $modules))); + parent::setUp(array_merge(array('ajax_test', 'ajax_forms_test'), $modules)); } /** - * Returns the passed-in commands array without the initial settings command. + * Search through a settings return; find/return a setting. * - * Depending on factors that may be irrelevant to a particular test, - * ajax_render() may prepend a settings command. This function allows the test - * to only have to concern itself with the commands that were passed to - * ajax_render(). + * @param $commands + * The AJAX commands returned by the server. + * @param $conditions + * Array of info we're looking to find in the return. + * @param $message + * An assertion message. */ - protected function discardSettings($commands) { - if ($commands[0]['command'] == 'settings') { - array_shift($commands); + protected function assertCommand($json, $conditions, $message) { + //it could be that we really want to test on the first array of the ajax + //handling so we should not remove the settings array + if ($json[0]['command'] == 'settings' && (count($json) > 1)) { + array_shift($json); + } + // @todo Counting success is bad. We have unique expectations. + // counting success should be outside of the foreach to not + // overwrite possible successes + $count_success = 0; + foreach ($json as $item) { + // Can't anticipate ajaxPageState. + if (isset($item['settings']['ajaxPageState'])) { + unset($item['settings']['ajaxPageState']); + } + + // @todo Compare $item to $conditions (rename to $command) to make any sense of this. + foreach ($conditions as $key => $value) { + // @todo This should be a type-agnostic comparison. But let's remove + // this entire inner foreach instead. + if (isset($item[$key]) && $item[$key] == $value) { + $count_success++; + } + } } - return $commands; + $this->assertEqual(count($conditions), $count_success, $message); } } @@ -42,27 +67,46 @@ class AJAXFrameworkTestCase extends AJAX * Test proper passing of JavaScript settings via ajax_render(). */ function testAJAXRender() { - $result = $this->drupalGetAJAX('ajax-test/render'); - // Verify that JavaScript settings are contained (always first). - $this->assertIdentical($result[0]['command'], 'settings', t('drupal_add_js() settings are contained first.')); - // Verify that basePath is contained in JavaScript settings. - $this->assertEqual($result[0]['settings']['basePath'], base_path(), t('Base path is contained in JavaScript settings.')); + $commands = $this->drupalGetAJAX('ajax-test/render'); + // Verify that there is a command to load settings added with + // drupal_add_js(). + + $result = $this->assertCommand($commands, array( + 'command' => 'settings', + ), t('ajax_render() loads settings added with drupal_add_js().')); + + $result = $this->assertCommand($commands, array( + 'settings' => array('basePath' => base_path(), 'ajax' => 'test'), + ), t('The %setting setting is included.', array('%setting' => 'basePath'))); + + // Verify that our file is included in the list. + $file = file_create_url('misc/drupal.js'); + foreach ($commands as $command) { + if ($command['command'] == 'scripts') { + $this->assertEqual($command['files'][$file], $file, t('The %file file is included.', array('%file' => 'misc/drupal.js'))); + } + } } /** * Test behavior of ajax_render_error(). */ function testAJAXRenderError() { - $result = $this->discardSettings($this->drupalGetAJAX('ajax-test/render-error')); + $commands = $this->drupalGetAJAX('ajax-test/render-error'); // Verify default error message. - $this->assertEqual($result[0]['command'], 'alert', t('ajax_render_error() invokes alert command.')); - $this->assertEqual($result[0]['text'], t('An error occurred while handling the request: The server received invalid input.'), t('Default error message is output.')); + $result = $this->assertCommand($commands, array( + 'command' => 'alert', + 'text' => t('An error occurred while handling the request: The server received invalid input.'), + ), t('ajax_render_error() invokes alert command.')); + // Verify custom error message. $edit = array( 'message' => 'Custom error message.', ); - $result = $this->discardSettings($this->drupalGetAJAX('ajax-test/render-error', array('query' => $edit))); - $this->assertEqual($result[0]['text'], $edit['message'], t('Custom error message is output.')); + $commands = $this->drupalGetAJAX('ajax-test/render-error', array('query' => $edit)); + $result = $this->assertCommand($commands, array( + 'text' => $edit['message'], + ), t('Custom error message is output.')); } } @@ -84,11 +128,16 @@ class AJAXCommandsTestCase extends AJAXT function testAJAXRender() { $commands = array(); $commands[] = ajax_command_settings(array('foo' => 42)); - $result = $this->drupalGetAJAX('ajax-test/render', array('query' => array('commands' => $commands))); - // Verify that JavaScript settings are contained (always first). - $this->assertIdentical($result[0]['command'], 'settings', t('drupal_add_js() settings are contained first.')); + + $commands = $this->drupalGetAJAX('ajax-test/render', array( + 'query' => array('commands' => $commands), + )); + // Verify that the custom setting is contained. - $this->assertEqual($result[1]['settings']['foo'], 42, t('Custom setting is output.')); + $result = $this->assertCommand($commands, array( + 'command' => 'settings', + 'settings' => array('foo' => 42), + ), t('Custom setting is output.')); } /** @@ -102,69 +151,104 @@ class AJAXCommandsTestCase extends AJAXT $edit = array(); // Tests the 'after' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'after' && $command['data'] == 'This will be placed after', "'after' AJAX command issued with correct data"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div"))); + $result = $this->assertCommand($commands, array( + 'command' => 'insert', + 'method' => 'after', + 'data' => 'This will be placed after', + ), "'after' AJAX command issued with correct data"); // Tests the 'alert' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'alert' && $command['text'] == 'Alert', "'alert' AJAX Command issued with correct text"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert"))); + $result = $this->assertCommand($commands, array( + 'command' => 'alert', + 'text' => 'Alert', + ), "'alert' AJAX Command issued with correct text"); // Tests the 'append' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'append' && $command['data'] == 'Appended text', "'append' AJAX command issued with correct data"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something"))); + $result = $this->assertCommand($commands, array( + 'command' => 'insert', + 'method' => 'append', + 'data' => 'Appended text', + ), "'append' AJAX command issued with correct data"); // Tests the 'before' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'before' && $command['data'] == 'Before text', "'before' AJAX command issued with correct data"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div"))); + $result = $this->assertCommand($commands, array( + 'command' => 'insert', + 'method' => 'before', + 'data' => 'Before text', + ), "'before' AJAX command issued with correct data"); // Tests the 'changed' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed.")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'changed' && $command['selector'] == '#changed_div', "'changed' AJAX command issued with correct selector"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed."))); + $result = $this->assertCommand($commands, array( + 'command' => 'changed', + 'selector' => '#changed_div', + ), "'changed' AJAX command issued with correct selector"); // Tests the 'changed' command using the second argument. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk.")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'changed' && $command['selector'] == '#changed_div' && $command['asterisk'] == '#changed_div_mark_this', "'changed' AJAX command (with asterisk) issued with correct selector"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk."))); + $result = $this->assertCommand($commands, array( + 'command' => 'changed', + 'selector' => '#changed_div', + 'asterisk' => '#changed_div_mark_this', + ), "'changed' AJAX command (with asterisk) issued with correct selector"); // Tests the 'css' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue.")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'css' && $command['selector'] == '#css_div' && $command['argument']['background-color'] == 'blue', "'css' AJAX command issued with correct selector"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue."))); + + $result = $this->assertCommand($commands, array( + 'command' => 'css', + 'selector' => '#css_div', + 'argument' => array('background-color' => 'blue'), + ), "'css' AJAX command issued with correct selector"); // Tests the 'data' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX data command: Issue command.")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'data' && $command['name'] == 'testkey' && $command['value'] == 'testvalue', "'data' AJAX command issued with correct key and value"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX data command: Issue command."))); + $result = $this->assertCommand($commands, array( + 'command' => 'data', + 'name' => 'testkey', + 'value' => 'testvalue', + ), "'data' AJAX command issued with correct key and value"); // Tests the 'html' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector.")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'html' && $command['data'] == 'replacement text', "'html' AJAX command issued with correct data"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector."))); + $result = $this->assertCommand($commands, array( + 'command' => 'insert', + 'method' => 'html', + 'data' => 'replacement text', + ), "'html' AJAX command issued with correct data"); // Tests the 'insert' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX insert: Let client insert based on #ajax['method'].")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'insert' && $command['method'] == NULL && $command['data'] == 'insert replacement text', "'insert' AJAX command issued with correct data"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX insert: Let client insert based on #ajax['method']."))); + $result = $this->assertCommand($commands, array( + 'command' => 'insert', + 'data' => 'insert replacement text', + ), "'insert' AJAX command issued with correct data"); // Tests the 'prepend' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'prepend' && $command['data'] == 'prepended text', "'prepend' AJAX command issued with correct data"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something"))); + $result = $this->assertCommand($commands, array( + 'command' => 'insert', + 'method' => 'prepend', + 'data' => 'prepended text', + ), "'prepend' AJAX command issued with correct data"); // Tests the 'remove' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'remove' && $command['selector'] == '#remove_text', "'remove' AJAX command issued with correct command and selector"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text"))); + $result = $this->assertCommand($commands, array( + 'command' => 'remove', + 'selector' => '#remove_text', + ), "'remove' AJAX command issued with correct command and selector"); // Tests the 'restripe' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'restripe' command")))); - $command = $commands[0]; - $this->assertTrue($command['command'] == 'restripe' && $command['selector'] == '#restripe_table', "'restripe' AJAX command issued with correct selector"); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'restripe' command"))); + $result = $this->assertCommand($commands, array( + 'command' => 'restripe', + 'selector' => '#restripe_table', + ), "'restripe' AJAX command issued with correct selector"); } } @@ -196,19 +280,21 @@ class AJAXFormValuesTestCase extends AJA $edit = array( 'select' => $item, ); - $commands = $this->discardSettings($this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select')); - $data_command = $commands[1]; - $this->assertEqual($data_command['value'], $item); + $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select'); + $result = $this->assertCommand($commands, array( + 'value' => $item, + ), "verification of AJAX form values from a selectbox issued with a correct value"); } - // Verify form values of a checkbox element. foreach (array(FALSE, TRUE) as $item) { $edit = array( 'checkbox' => $item, ); - $commands = $this->discardSettings($this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'checkbox')); - $data_command = $commands[1]; - $this->assertEqual((int) $data_command['value'], (int) $item); + $commands = $this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'checkbox'); + + $result = $this->assertCommand($commands, array( + 'value' => $item, + ), "verification of AJAX form values from a checkbox issued with a correct value"); } } } @@ -246,12 +332,14 @@ class AJAXMultiFormTestCase extends AJAX // 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. @@ -268,9 +356,12 @@ class AJAXMultiFormTestCase extends AJAX // 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_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.')); + //@todo When NULL is given as a value the assertFieldByXPath doesn't work $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'); @@ -278,7 +369,7 @@ class AJAXMultiFormTestCase extends AJAX // Submit the "add more" button of each form twice. After each corresponding // page update, ensure the same as above. foreach ($field_xpaths as $form_html_id => $field_xpath) { - for ($i=0; $i<2; $i++) { + for ($i = 0; $i < 2; $i++) { $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.')); @@ -309,9 +400,7 @@ class AJAXElementValidation extends AJAX * AJAX-enabled field fails due to the required field being empty. */ function testAJAXElementValidation() { - $web_user = $this->drupalCreateUser(); $edit = array('drivertext' => t('some dumb text')); - // Post with 'drivertext' as the triggering element. $post_result = $this->drupalPostAJAX('ajax_validation_test', $edit, 'drivertext'); // Look for a validation failure in the resultant JSON. @@ -319,4 +408,3 @@ class AJAXElementValidation extends AJAX $this->assertText('ajax_forms_test_validation_form_callback invoked', t('The correct callback was invoked')); } } - Index: modules/simpletest/tests/ajax_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax_test.module,v retrieving revision 1.3 diff -u -p -r1.3 ajax_test.module --- modules/simpletest/tests/ajax_test.module 13 Mar 2010 06:55:50 -0000 1.3 +++ modules/simpletest/tests/ajax_test.module 30 Sep 2010 23:16:25 -0000 @@ -31,7 +31,8 @@ function ajax_test_menu() { * Menu callback; Returns $_GET['commands'] suitable for use by ajax_deliver(). * * Additionally ensures that ajax_render() incorporates JavaScript settings - * by invoking drupal_add_js() with a dummy setting. + * and files by invoking drupal_add_js() with a dummy setting, which causes + * drupal_add_js() to also automatically add "misc/drupal.js". */ function ajax_test_render() { // Prepare AJAX commands. Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.967 diff -u -p -r1.967 system.module --- modules/system/system.module 29 Sep 2010 16:06:11 -0000 1.967 +++ modules/system/system.module 30 Sep 2010 23:16:32 -0000 @@ -514,6 +514,7 @@ function system_menu() { 'page callback' => 'ajax_form_callback', 'delivery callback' => 'ajax_deliver', 'access callback' => TRUE, + 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, 'file path' => 'includes', 'file' => 'form.inc',