1<?php 2 3namespace Drupal\Tests\system\Functional\Ajax; 4 5use Drupal\Component\Serialization\Json; 6use Drupal\Core\Ajax\AddCssCommand; 7use Drupal\Core\Ajax\AlertCommand; 8use Drupal\Core\Ajax\AppendCommand; 9use Drupal\Core\Ajax\HtmlCommand; 10use Drupal\Core\Ajax\PrependCommand; 11use Drupal\Core\Ajax\SettingsCommand; 12use Drupal\Core\Asset\AttachedAssets; 13use Drupal\Core\EventSubscriber\MainContentViewSubscriber; 14use Drupal\Tests\BrowserTestBase; 15 16/** 17 * Performs tests on AJAX framework functions. 18 * 19 * @group Ajax 20 */ 21class FrameworkTest extends BrowserTestBase { 22 23 /** 24 * {@inheritdoc} 25 */ 26 protected $defaultTheme = 'stark'; 27 28 /** 29 * {@inheritdoc} 30 */ 31 protected static $modules = ['node', 'ajax_test', 'ajax_forms_test']; 32 33 /** 34 * Verifies the Ajax rendering of a command in the settings. 35 */ 36 public function testAJAXRender() { 37 // Verify that settings command is generated if JavaScript settings exist. 38 $commands = $this->drupalGetAjax('ajax-test/render'); 39 $expected = new SettingsCommand(['ajax' => 'test'], TRUE); 40 $this->assertCommand($commands, $expected->render(), 'JavaScript settings command is present.'); 41 } 42 43 /** 44 * Tests AjaxResponse::prepare() AJAX commands ordering. 45 */ 46 public function testOrder() { 47 $expected_commands = []; 48 49 // Expected commands, in a very specific order. 50 $asset_resolver = \Drupal::service('asset.resolver'); 51 $css_collection_renderer = \Drupal::service('asset.css.collection_renderer'); 52 $js_collection_renderer = \Drupal::service('asset.js.collection_renderer'); 53 $renderer = \Drupal::service('renderer'); 54 $build['#attached']['library'][] = 'ajax_test/order-css-command'; 55 $assets = AttachedAssets::createFromRenderArray($build); 56 $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE)); 57 $expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array)); 58 $build['#attached']['library'][] = 'ajax_test/order-header-js-command'; 59 $build['#attached']['library'][] = 'ajax_test/order-footer-js-command'; 60 $assets = AttachedAssets::createFromRenderArray($build); 61 list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE); 62 $js_header_render_array = $js_collection_renderer->render($js_assets_header); 63 $js_footer_render_array = $js_collection_renderer->render($js_assets_footer); 64 $expected_commands[2] = new PrependCommand('head', $js_header_render_array); 65 $expected_commands[3] = new AppendCommand('body', $js_footer_render_array); 66 $expected_commands[4] = new HtmlCommand('body', 'Hello, world!'); 67 68 // Load any page with at least one CSS file, at least one JavaScript file 69 // and at least one #ajax-powered element. The latter is an assumption of 70 // drupalPostAjaxForm(), the two former are assumptions of the Ajax 71 // renderer. 72 // @todo refactor AJAX Framework + tests to make less assumptions. 73 $this->drupalGet('ajax_forms_test_lazy_load_form'); 74 75 // Verify AJAX command order — this should always be the order: 76 // 1. CSS files 77 // 2. JavaScript files in the header 78 // 3. JavaScript files in the footer 79 // 4. Any other AJAX commands, in whatever order they were added. 80 $commands = $this->drupalGetAjax('ajax-test/order'); 81 $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[1]->render()); 82 $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[2]->render()); 83 $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[3]->render()); 84 $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[4]->render()); 85 } 86 87 /** 88 * Tests the behavior of an error alert command. 89 */ 90 public function testAJAXRenderError() { 91 // Verify custom error message. 92 $edit = [ 93 'message' => 'Custom error message.', 94 ]; 95 $commands = $this->drupalGetAjax('ajax-test/render-error', ['query' => $edit]); 96 $expected = new AlertCommand($edit['message']); 97 $this->assertCommand($commands, $expected->render(), 'Custom error message is output.'); 98 } 99 100 /** 101 * Asserts the array of Ajax commands contains the searched command. 102 * 103 * An AjaxResponse object stores an array of Ajax commands. This array 104 * sometimes includes commands automatically provided by the framework in 105 * addition to commands returned by a particular controller. During testing, 106 * we're usually interested that a particular command is present, and don't 107 * care whether other commands precede or follow the one we're interested in. 108 * Additionally, the command we're interested in may include additional data 109 * that we're not interested in. Therefore, this function simply asserts that 110 * one of the commands in $haystack contains all of the keys and values in 111 * $needle. Furthermore, if $needle contains a 'settings' key with an array 112 * value, we simply assert that all keys and values within that array are 113 * present in the command we're checking, and do not consider it a failure if 114 * the actual command contains additional settings that aren't part of 115 * $needle. 116 * 117 * @param $haystack 118 * An array of rendered Ajax commands returned by the server. 119 * @param $needle 120 * Array of info we're expecting in one of those commands. 121 */ 122 protected function assertCommand($haystack, $needle) { 123 $found = FALSE; 124 foreach ($haystack as $command) { 125 // If the command has additional settings that we're not testing for, do 126 // not consider that a failure. 127 if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) { 128 $command['settings'] = array_intersect_key($command['settings'], $needle['settings']); 129 } 130 // If the command has additional data that we're not testing for, do not 131 // consider that a failure. Also, == instead of ===, because we don't 132 // require the key/value pairs to be in any particular order 133 // (http://php.net/manual/language.operators.array.php). 134 if (array_intersect_key($command, $needle) == $needle) { 135 $found = TRUE; 136 break; 137 } 138 } 139 $this->assertTrue($found); 140 } 141 142 /** 143 * Requests a path or URL in drupal_ajax format and JSON-decodes the response. 144 * 145 * @param \Drupal\Core\Url|string $path 146 * Drupal path or URL to request from. 147 * @param array $options 148 * Array of URL options. 149 * @param array $headers 150 * Array of headers. 151 * 152 * @return array 153 * Decoded JSON. 154 */ 155 protected function drupalGetAjax($path, array $options = [], array $headers = []) { 156 $headers[] = 'X-Requested-With: XMLHttpRequest'; 157 if (!isset($options['query'][MainContentViewSubscriber::WRAPPER_FORMAT])) { 158 $options['query'][MainContentViewSubscriber::WRAPPER_FORMAT] = 'drupal_ajax'; 159 } 160 return Json::decode($this->drupalGet($path, $options, $headers)); 161 } 162 163} 164