diff --git includes/ajax.inc includes/ajax.inc index 74b4e73..56d552d 100644 --- includes/ajax.inc +++ includes/ajax.inc @@ -193,12 +193,12 @@ * 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); - if (!empty($scripts['settings'])) { - array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $scripts['settings']['data']))); - } + // AJAX responses are returned as JSON and not themed via html.tpl.php. Thus, + // markup returned by drupal_get_js() is not contained, but the client needs + // to know the JavaScript settings and files required by the new content, so + // we call ajax_get_js() to get that. + $commands = array_merge(ajax_get_js(), $commands); + $commands = array_merge(ajax_get_css(), $commands); // Allow modules to alter any AJAX response. drupal_alter('ajax_render', $commands); @@ -207,6 +207,246 @@ function ajax_render($commands = array()) { } /** + * Return javascript files on the current page for use with with AJAX. + * + * This function is invoked during normal HTML page loads to tell the client + * what javascript files are in use on the page. This function is also invoked + * during AJAX operations to tell the client what javascript files may + * need to be added to the page. + * + * This function is the equivalent of drupal_get_js(), with the following + * differences: + * + * - By default, all scopes are processed at once, as AJAX responses typically + * do not contain the same kind of 'header' and 'footer' scopes like full + * page responses. + * - Files are not aggregated, because we do not know, which source files have + * already been loaded as part of the base page. The client-side code will + * determine new files and only load those. + * - Query_strings are not used here as they can cause files to be loaded via + * ajax more than once if they change. We never want this, + * even if the file has changed. + * + * @param $type + * Either 'command' to get a command to send to the browser, or 'files' to + * get a list of javascript files. + * @param $scope + * (optional) The scope for which the JavaScript code should be returned. If + * not specified, returns the code for all scopes. + * @param $javascript + * (optional) An array with all JavaScript code. Defaults to the default + * JavaScript array for the given scope. + * + * @return + * An array of commands suitable for use with the ajax_render() function. + * + * @see drupal_get_js() + */ +function ajax_get_js($type = 'command', $scope = NULL, $javascript = NULL) { + if (!isset($javascript)) { + // Get the static directly so that the 'alter' if used below will + // be permanent. + $javascript = &drupal_static('drupal_add_js', array()); + } + if (empty($javascript)) { + return array(); + } + + if ($type == 'command') { + // Allow modules to alter the JavaScript. + drupal_alter('js', $javascript); + } + + // Filter out elements of the given scope. + if (isset($scope)) { + $items = array(); + foreach ($javascript as $item) { + if ($item['scope'] == $scope) { + $items[] = $item; + } + } + } + else { + $items = array_values($javascript); + } + + uasort($items, 'drupal_sort_weight'); + + foreach ($items as $item) { + switch ($item['type']) { + case 'setting': + // drupal_add_js() puts all settings into a single $item of type + // 'setting', so we can do a simple assignment here, rather than adding + // to an array. + $settings = $item['data']; + break; + + case 'inline': + // @todo Presently, inline JavaScript code is ignored and not returned + // to the client. Evaluate the use-cases of AJAX responses that depend + // on inline JavaScript code to determine how to best handle that. + break; + + case 'file': + $file = file_create_url($item['data']); + $js_files[$file] = $file; + break; + + case 'external': + $js_files[$item['data']] = $item['data']; + break; + } + } + + switch ($type) { + case 'command': + $commands = array(); + if (!empty($settings)) { + $commands[] = ajax_command_settings(call_user_func_array('array_merge_recursive', $settings)); + } + if (!empty($js_files)) { + $commands[] = ajax_command_scripts($js_files); + } + return $commands; + + case 'files': + default: + return $js_files; + } +} + +/** + * Return a list of all CSS files used in the current page load for use with AJAX. + * + * This function is invoked during normal HTML page loads to tell the client + * what javascript files are in use on the page. This function is also invoked + * during AJAX operations to tell the client what javascript files may + * need to be added to the page. + * + * This function is equivalent to drupal_get_css() with the following differences: + * + * - Files are not aggregated. + * - Query strings are ignored. + * + * @param $type + * Either 'command' to get a command to send to the browser, or 'files' to + * get a list of css files. + * @param $css + * (optional) An array of CSS files. If no array is provided, the default + * stylesheets array is used instead. + * + * @return + * A string of XHTML CSS tags. + */ +function ajax_get_css($type = 'command', $css = NULL) { + if (!isset($css)) { + // Get the static directly so that the 'alter' if used below will + // be permanent. + $css = &drupal_static('drupal_add_css', array()); + } + + if ($type == 'command') { + // Allow modules and themes to alter the CSS items. + drupal_alter('css', $css); + } + + // Sort CSS items according to their weights. + uasort($css, 'drupal_sort_weight'); + + // Remove the overridden CSS files. Later CSS files override former ones. + $previous_item = array(); + foreach ($css as $key => $item) { + // Once the weight gets to 100, we're in theme CSS. We must stop here, + // because theme CSS files will not be added via AJAX. + if ($item['weight'] >= 100) { + break; + } + + if ($item['type'] == 'file') { + // If defined, force a unique basename for this file. + $basename = isset($item['basename']) ? $item['basename'] : basename($item['data']); + if (isset($previous_item[$basename])) { + // Remove the previous item that shared the same base name. + unset($css[$previous_item[$basename]]); + } + $previous_item[$basename] = $key; + } + } + + $css_files = array(); + foreach ($css as $name => $file) { + if ($file['type'] == 'file' || $file['type'] == 'external') { + $css_files[$name] = array( + 'href' => $file['data'], + 'media' => $file['media'], + 'browsers' => $file['browsers'], + ); + } + } + + switch ($type) { + case 'command': + $commands = array(); + if (!empty($css_files)) { + $commands[] = ajax_command_css_files($css_files); + } + return $commands; + + case 'files': + default: + return $css_files; + } + +} + +/** + * Add any last additional info to the footer needed for AJAX operations. + * + * If javascript is enabled on the page, we need to provide a list of all + * CSS and javascript files that we know about on the page so that they will + * not be included again in ajax operations. + * + * @return + * A string that is to be appended in the footer of the page containing + * javascript to inform the browser which .js and .css files are known + * to be in use. + */ +function ajax_get_footer() { + $output = ''; + $js_files = ajax_get_js('files'); + $css_files = array(); + + // Only bother doing this if there are javascript files. + if ($js_files) { + // For inline Javascript to validate as XHTML, all Javascript containing + // XHTML needs to be wrapped in CDATA. To make that backwards compatible + // with HTML 4, we need to comment out the CDATA-tag. + $loaded = array('ajaxFiles' => array('scripts' => $js_files)); + + $css_files = ajax_get_css('files'); + if ($css_files) { + $loaded['ajaxFiles']['css'] = $css_files; + } + + + $element = array( + '#tag' => 'script', + '#value' => '', + '#value_prefix' => "\n\n", + '#value' => 'jQuery.extend(Drupal.settings, ' . drupal_json_encode($loaded) . ");", + '#attributes' => array( + 'type' => 'text/javascript', + ), + ); + + $output .= theme('html_tag', array('element' => $element)); + } + + return $output; +} + +/** * Get a form submitted via #ajax during an AJAX callback. * * This will load a form from the form cache used during AJAX operations. It @@ -955,3 +1195,50 @@ function ajax_command_restripe($selector) { ); } +/** + * Creates scripts command for the AJAX responder. + * + * This will add JavaScript files to the output. Files that have already been + * processed will not be processed again. + * + * This command is implemented by Drupal.ajax.prototype.commands.scripts() + * defined in misc/ajax.js. + * + * @param $files + * The $files array is a simple array of filenames that match, exactly, the + * scripts as they are added to the page or will be added to the page in the + * future. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_scripts($files) { + return array( + 'command' => 'scripts', + 'files' => $files, + ); +} + +/** + * Creates css command for the AJAX responder. + * + * This will add CSS files to the output. Files that have already been + * processed will not be processed again. + * + * This command is implemented by Drupal.ajax.prototype.commands.css() + * defined in misc/ajax.js. + * + * @param $files + * The $files array is a simple array of filenames that match, exactly, the + * css files as they are added to the page or will be added to the page in the + * future. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_css_files($files) { + return array( + 'command' => 'css_files', + 'files' => $files, + ); +} diff --git includes/common.inc includes/common.inc index 9fc82bb..c2dd076 100644 --- includes/common.inc +++ includes/common.inc @@ -2816,7 +2816,7 @@ function drupal_add_css($data = NULL, $options = NULL) { */ function drupal_get_css($css = NULL) { if (!isset($css)) { - $css = drupal_add_css(); + $css = &drupal_static('drupal_add_css', array()); } // Allow modules and themes to alter the CSS items. @@ -3788,7 +3788,9 @@ function drupal_js_defaults($data = NULL) { */ function drupal_get_js($scope = 'header', $javascript = NULL) { if (!isset($javascript)) { - $javascript = drupal_add_js(); + // Get the static directly so that the 'alter' if used below will + // be permanent. + $javascript = &drupal_static('drupal_add_js', array()); } if (empty($javascript)) { return ''; @@ -3842,6 +3844,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { 'type' => 'text/javascript', ), ); + foreach ($items as $item) { $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; @@ -3867,12 +3870,13 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { case 'file': $js_element = $element; + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $uri = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); if (!$item['preprocess'] || !$preprocess_js) { - if ($item['defer']) { - $js_element['#attributes']['defer'] = 'defer'; - } - $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; - $js_element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); + $js_element['#attributes']['src'] = $uri; $processed[$index++] = theme('html_tag', array('element' => $js_element)); } else { @@ -3881,6 +3885,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { $key = 'aggregate' . $index; $processed[$key] = ''; $files[$key][$item['data']] = $item; + $reported_files[$uri] = 1; } break; @@ -3900,7 +3905,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { if ($preprocess_js && count($files) > 0) { foreach ($files as $key => $file_set) { $uri = drupal_build_js_cache($file_set); - // Only include the file if was written successfully. Errors are logged + // Only include the file if it was written successfully. Errors are logged // using watchdog. if ($uri) { $preprocess_file = file_create_url($uri); @@ -6134,7 +6139,7 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { } if (!property_exists($object, $field)) { - // Skip fields that are not provided, default values are already known + // Skip fields that are not provided, default values are already known // by the database. continue; } diff --git includes/theme.inc includes/theme.inc index e325ad7..5d623fc 100644 --- includes/theme.inc +++ includes/theme.inc @@ -2002,7 +2002,7 @@ function theme_indentation($variables) { /** * Returns HTML output for a single table cell for theme_table(). - * + * * @param $cell * Array of cell information, or string to display in cell. * @param bool $header @@ -2292,6 +2292,7 @@ function template_process_html(&$variables) { // Place the rendered HTML for the page body into a top level variable. $variables['page'] = $variables['page']['#children']; $variables['page_bottom'] .= drupal_get_js('footer'); + $variables['page_bottom'] .= ajax_get_footer(); $variables['head'] = drupal_get_html_head(); $variables['css'] = drupal_add_css(); diff --git misc/ajax.js misc/ajax.js index 553d2cc..b544147 100644 --- misc/ajax.js +++ misc/ajax.js @@ -16,6 +16,14 @@ Drupal.ajax = Drupal.ajax || {}; /** + * Holds a list of files known to be on the page so that AJAX requests can + * selectively load additionally required files later on. + * + * @see Drupal.ajax.prototype.commands.scripts() + */ +Drupal.ajaxFiles = Drupal.ajaxFiles || { scripts: {}, css: {} }; + +/** * Attaches the AJAX behavior to each AJAX form element. */ Drupal.behaviors.AJAX = { @@ -427,6 +435,69 @@ Drupal.ajax.prototype.commands = { }, /** + * Command to add scripts to the page. + * + * Keeps track of files already on the page and attempts to only load + * additionally required scripts. + */ + scripts: function (ajax, response, status) { + // Build a list of scripts already loaded. Aggregated files have already + // been registered in Drupal.ajaxFiles.scripts via drupal_get_js(). + // Non-aggregated files can simply be enumerated by script tags on the page. + $('script').each(function () { + Drupal.ajaxFiles.scripts[Drupal.getPath(this.src)] = this.src; + }); + + var html = ''; + for (var file in response.files) { + // @todo Files in this stack are based on their 'src' attribute and/or a + // previous AJAX request. Their URIs may contain query strings, domain + // names, and other additions that can conflict with this simple key + // check. Consider to store and use basenames? + if (!Drupal.ajaxFiles.scripts[file]) { + Drupal.ajaxFiles.scripts[file] = file; + html += ''; + } + } + if (html) { + $('html').prepend(html); + } + }, + + /** + * Command to add CSS files to the page. + * + * Keeps track of files already on the page. Additionally, if a previous + * css_files command (During AJAX) added CSS to the page, this will be + * removed before new files are added, in order to keep IE from exploding + * with file after file. + */ + css_files: function(ajax, response, status) { + // Build a list of css files already loaded: + $('link:not(.drupal-temporary-css)').each(function () { + if ($(this).attr('type') == 'text/css') { + var link = Drupal.getPath($(this).attr('href')); + if (link) { + Drupal.ajaxFiles.css[link] = $(this).attr('href'); + } + } + }); + + var html = ''; + for (var i in response.files) { + if (!Drupal.ajaxFiles.css[response.files[i].href]) { + html += ''; + } + } + + if (html) { + $('link.drupal-temporary-css').remove(); + $('body').append($(html)); + } + }, + + /** * Command to attach data using jQuery's data API. */ data: function (ajax, response, status) { @@ -447,4 +518,14 @@ Drupal.ajax.prototype.commands = { } }; +/** + * Only on the beginning of the page, not using a behavior, add files that + * may have been specified in the footer of the page to our internal array. + */ + +$(function () { + if (Drupal.settings.ajaxFiles && Drupal.settings.ajaxFiles.scripts) { + $.extend(Drupal.ajaxFiles.scripts, Drupal.settings.ajaxFiles.scripts); + } +}); })(jQuery); diff --git misc/drupal.js misc/drupal.js index 7d0d068..ef45499 100644 --- misc/drupal.js +++ misc/drupal.js @@ -325,6 +325,22 @@ Drupal.ajaxError = function (xmlhttp, uri) { return message; }; +/** + * Get the actual path of a link, dropping items after the ? + */ +Drupal.getPath = function (link) { + if (!link) { + return; + } + + var index = link.indexOf('?'); + if (index != -1) { + link = link.substr(0, index); + } + + return link; +} + // Class indicating that JS is enabled; used for styling purpose. $('html').addClass('js'); diff --git modules/field/tests/field.test modules/field/tests/field.test index 8e8bd73..a0d5044 100644 --- modules/field/tests/field.test +++ modules/field/tests/field.test @@ -1439,7 +1439,7 @@ class FieldFormTestCase extends FieldTestCase { // 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']; + $this->content = $commands[2]['data']; for ($delta = 0; $delta <= $delta_range; $delta++) { $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); diff --git modules/poll/poll.test modules/poll/poll.test index b16a676..3918dc9 100644 --- modules/poll/poll.test +++ modules/poll/poll.test @@ -342,7 +342,7 @@ class PollJSAddChoice extends DrupalWebTestCase { // Press 'add choice' button through AJAX, and place the expected HTML result // as the tested content. $commands = $this->drupalPostAJAX(NULL, $edit, array('op' => t('More choices'))); - $this->content = $commands[1]['data']; + $this->content = $commands[2]['data']; $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))); diff --git modules/simpletest/tests/ajax.test modules/simpletest/tests/ajax.test index 232074f..f227d5d 100644 --- modules/simpletest/tests/ajax.test +++ modules/simpletest/tests/ajax.test @@ -1,5 +1,6 @@ $value) { + //compare both key and value towards our data that was given + if (isset($item[$key]) && ($item[$key] == $value)) { + $count_success++; + } + } + if(sizeof($conditions) == $count_success) { + $found = TRUE; + } + } + return $found; + } } /** @@ -42,27 +75,50 @@ class AJAXFrameworkTestCase extends AJAXTestCase { * 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->findJSONKey(array('command' => 'settings'), $commands); + $this->assertTrue($result, t('ajax_render() added a settings command to load settings added with drupal_add_js().')); + $result = $this->findJSONKey(array('settings' => array('basePath' => base_path(), 'ajax' => 'test')), $commands); + $this->assertTrue($result, t('The %setting setting is included.', array('%setting' => 'basePath'))); + + // Verify that there is a command to load script files added with + // drupal_add_js(). + $result = $this->findJSONKey(array('command' => 'scripts'), $commands); + $this->assertTrue($result, t('ajax_render() added a scripts command to load files added with drupal_add_js().')); + + $file = file_create_url('misc/drupal.js'); + + //find the files array + foreach ($commands as $command) { + if($command['command'] == 'scripts') { + //Verify that our file is included in the list + $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->discardSettings($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->findJSONKey(array('command' => 'alert', 'text' => t('An error occurred while handling the request: The server received invalid input.')), $commands); + $this->assertTrue($result, t('ajax_render_error() invokes alert command.')); + + //$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.')); // 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->discardSettings($this->drupalGetAJAX('ajax-test/render-error', array('query' => $edit))); + $result = $this->findJSONKey(array('text' => $edit['message']), $commands); + $this->assertTrue($result, t('Custom error message is output.')); + //$this->assertEqual($result[0]['text'], $edit['message'], t('Custom error message is output.')); } } @@ -84,11 +140,12 @@ class AJAXCommandsTestCase extends AJAXTestCase { 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.')); + // discardSettings() discards only what is automatically added by + // ajax_render(), not the one added by the ajax-test/render callback. + $result = $this->discardSettings($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->findJSONKey(array('settings' => array('foo' => 42)), $commands); + $this->assertTrue($result, t('Custom setting is output.')); } /** @@ -103,68 +160,69 @@ class AJAXCommandsTestCase extends AJAXTestCase { // 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"); + $result = $this->findJSONKey(array('command' => 'insert', 'method' => 'after', 'data' => 'This will be placed after'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'alert', 'text' => 'Alert'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'insert', 'method' => 'append', 'data' => 'Appended text'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'insert', 'method' => 'before', 'data' => 'Before text'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'changed', 'selector' => '#changed_div'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'changed', 'selector' => '#changed_div', 'asterisk' => '#changed_div_mark_this'), $commands); + $this->assertTrue($result, "'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"); + + $result = $this->findJSONKey(array('command' => 'css', 'selector' => '#css_div', 'argument' => array('background-color' => 'blue')), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'data', 'name' => 'testkey', 'value' => 'testvalue'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'insert', 'method' => 'html', 'data' => 'replacement text'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'insert', 'data' => 'insert replacement text'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'insert', 'method' => 'prepend', 'data' => 'prepended text'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'remove', 'selector' => '#remove_text'), $commands); + $this->assertTrue($result, "'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"); + $result = $this->findJSONKey(array('command' => 'restripe', 'selector' => '#restripe_table'), $commands); + $this->assertTrue($result, "'restripe' AJAX command issued with correct selector"); } } @@ -197,8 +255,8 @@ class AJAXFormValuesTestCase extends AJAXTestCase { 'select' => $item, ); $commands = $this->discardSettings($this->drupalPostAJAX('ajax_forms_test_get_form', $edit, 'select')); - $data_command = $commands[1]; - $this->assertEqual($data_command['value'], $item); + $result = $this->findJSONKey(array('value' => $item), $commands); + $this->assertTrue($result, "verification of AJAX form values from a selectbox issued with a correct value"); } // Verify form values of a checkbox element. @@ -207,8 +265,8 @@ class AJAXFormValuesTestCase extends AJAXTestCase { '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); + $result = $this->findJSONKey(array('value' => (int)$item), $commands); + $this->assertTrue($result, "verification of AJAX form values from a checkbox issued with a correct value"); } } } @@ -281,12 +339,17 @@ class AJAXMultiFormTestCase extends AJAXTestCase { // 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']); + foreach($commands as $key => $command) { + if(isset($command['settings']) && $command['command'] == 'settings') { + $settings = array_merge_recursive($settings, $command['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.')); $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other'); @@ -327,3 +390,4 @@ class AJAXElementValidation extends AJAXTestCase { } } + diff --git modules/simpletest/tests/ajax_test.module modules/simpletest/tests/ajax_test.module index 46d36b0..09b9ba2 100644 --- modules/simpletest/tests/ajax_test.module +++ modules/simpletest/tests/ajax_test.module @@ -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.