1<?php 2 3namespace Drupal\Tests\taxonomy\Functional; 4 5use Drupal\Component\Serialization\Json; 6use Drupal\Core\Entity\Entity\EntityFormDisplay; 7use Drupal\Core\Entity\Entity\EntityViewDisplay; 8use Drupal\Core\Field\FieldStorageDefinitionInterface; 9use Drupal\field\Entity\FieldConfig; 10use Drupal\field\Entity\FieldStorageConfig; 11 12/** 13 * Tests the autocomplete implementation of the taxonomy class. 14 * 15 * @group taxonomy 16 */ 17class TermAutocompleteTest extends TaxonomyTestBase { 18 19 /** 20 * Modules to enable. 21 * 22 * @var array 23 */ 24 protected static $modules = ['node']; 25 26 /** 27 * {@inheritdoc} 28 */ 29 protected $defaultTheme = 'stark'; 30 31 /** 32 * The vocabulary. 33 * 34 * @var \Drupal\taxonomy\Entity\Vocabulary 35 */ 36 protected $vocabulary; 37 38 /** 39 * The field to add to the content type for the taxonomy terms. 40 * 41 * @var string 42 */ 43 protected $fieldName; 44 45 /** 46 * The admin user. 47 * 48 * @var \Drupal\user\Entity\User 49 */ 50 protected $adminUser; 51 52 /** 53 * The autocomplete URL to call. 54 * 55 * @var string 56 */ 57 protected $autocompleteUrl; 58 59 /** 60 * The term IDs indexed by term names. 61 * 62 * @var array 63 */ 64 protected $termIds; 65 66 /** 67 * {@inheritdoc} 68 */ 69 protected function setUp(): void { 70 parent::setUp(); 71 72 // Create a vocabulary. 73 $this->vocabulary = $this->createVocabulary(); 74 75 // Create 11 terms, which have some sub-string in common, in a 76 // non-alphabetical order, so that we will have more than 10 matches later 77 // when we test the correct number of results is returned, and we can test 78 // the order of the results. The location of the sub-string to match varies 79 // also, since it should not be necessary to start with the sub-string to 80 // match it. Save term IDs to reuse later. 81 $termNames = [ 82 'aaa 20 bbb', 83 'aaa 70 bbb', 84 'aaa 10 bbb', 85 'aaa 12 bbb', 86 'aaa 40 bbb', 87 'aaa 11 bbb', 88 'aaa 30 bbb', 89 'aaa 50 bbb', 90 'aaa 80', 91 'aaa 90', 92 'bbb 60 aaa', 93 ]; 94 foreach ($termNames as $termName) { 95 $term = $this->createTerm($this->vocabulary, ['name' => $termName]); 96 $this->termIds[$termName] = $term->id(); 97 } 98 99 // Create a taxonomy_term_reference field on the article Content Type that 100 // uses a taxonomy_autocomplete widget. 101 $this->fieldName = mb_strtolower($this->randomMachineName()); 102 FieldStorageConfig::create([ 103 'field_name' => $this->fieldName, 104 'entity_type' => 'node', 105 'type' => 'entity_reference', 106 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 107 'settings' => [ 108 'target_type' => 'taxonomy_term', 109 ], 110 ])->save(); 111 FieldConfig::create([ 112 'field_name' => $this->fieldName, 113 'bundle' => 'article', 114 'entity_type' => 'node', 115 'settings' => [ 116 'handler' => 'default', 117 'handler_settings' => [ 118 // Restrict selection of terms to a single vocabulary. 119 'target_bundles' => [ 120 $this->vocabulary->id() => $this->vocabulary->id(), 121 ], 122 ], 123 ], 124 ])->save(); 125 EntityFormDisplay::load('node.article.default') 126 ->setComponent($this->fieldName, [ 127 'type' => 'entity_reference_autocomplete', 128 ]) 129 ->save(); 130 EntityViewDisplay::load('node.article.default') 131 ->setComponent($this->fieldName, [ 132 'type' => 'entity_reference_label', 133 ]) 134 ->save(); 135 136 // Create a user and then login. 137 $this->adminUser = $this->drupalCreateUser(['create article content']); 138 $this->drupalLogin($this->adminUser); 139 140 // Retrieve the autocomplete url. 141 $this->drupalGet('node/add/article'); 142 $field = $this->assertSession()->fieldExists("{$this->fieldName}[0][target_id]"); 143 $this->autocompleteUrl = $this->getAbsoluteUrl($field->getAttribute('data-autocomplete-path')); 144 } 145 146 /** 147 * Helper function for JSON formatted requests. 148 * 149 * @param string|\Drupal\Core\Url $path 150 * Drupal path or URL to load into Mink controlled browser. 151 * @param array $options 152 * (optional) Options to be forwarded to the url generator. 153 * @param string[] $headers 154 * (optional) An array containing additional HTTP request headers. 155 * 156 * @return string[] 157 * Array representing decoded JSON response. 158 */ 159 protected function drupalGetJson($path, array $options = [], array $headers = []) { 160 $options = array_merge_recursive(['query' => ['_format' => 'json']], $options); 161 return Json::decode($this->drupalGet($path, $options, $headers)); 162 } 163 164 /** 165 * Tests that the autocomplete method returns the good number of results. 166 * 167 * @see \Drupal\taxonomy\Controller\TermAutocompleteController::autocomplete() 168 */ 169 public function testAutocompleteCountResults() { 170 // Test that no matching term found. 171 $data = $this->drupalGetJson( 172 $this->autocompleteUrl, 173 ['query' => ['q' => 'zzz']] 174 ); 175 $this->assertTrue(empty($data), 'Autocomplete returned no results'); 176 177 // Test that only one matching term found, when only one matches. 178 $data = $this->drupalGetJson( 179 $this->autocompleteUrl, 180 ['query' => ['q' => 'aaa 10']] 181 ); 182 $this->assertCount(1, $data, 'Autocomplete returned 1 result'); 183 184 // Test the correct number of matches when multiple are partial matches. 185 $data = $this->drupalGetJson( 186 $this->autocompleteUrl, 187 ['query' => ['q' => 'aaa 1']] 188 ); 189 $this->assertCount(3, $data, 'Autocomplete returned 3 results'); 190 191 // Tests that only 10 results are returned, even if there are more than 10 192 // matches. 193 $data = $this->drupalGetJson( 194 $this->autocompleteUrl, 195 ['query' => ['q' => 'aaa']] 196 ); 197 $this->assertCount(10, $data, 'Autocomplete returned only 10 results (for over 10 matches)'); 198 } 199 200 /** 201 * Tests that the autocomplete method returns properly ordered results. 202 * 203 * @see \Drupal\taxonomy\Controller\TermAutocompleteController::autocomplete() 204 */ 205 public function testAutocompleteOrderedResults() { 206 $expectedResults = [ 207 'aaa 10 bbb', 208 'aaa 11 bbb', 209 'aaa 12 bbb', 210 'aaa 20 bbb', 211 'aaa 30 bbb', 212 'aaa 40 bbb', 213 'aaa 50 bbb', 214 'aaa 70 bbb', 215 'bbb 60 aaa', 216 ]; 217 // Build $expected to match the autocomplete results. 218 $expected = []; 219 foreach ($expectedResults as $termName) { 220 $expected[] = [ 221 'value' => $termName . ' (' . $this->termIds[$termName] . ')', 222 'label' => $termName, 223 ]; 224 } 225 226 $data = $this->drupalGetJson( 227 $this->autocompleteUrl, 228 ['query' => ['q' => 'bbb']] 229 ); 230 231 $this->assertSame($expected, $data); 232 } 233 234} 235