3 namespace Drupal\KernelTests\Core\Menu;
5 use Drupal\Component\Plugin\Exception\PluginException;
6 use Drupal\Core\Menu\MenuTreeParameters;
7 use Drupal\Core\Menu\MenuTreeStorage;
8 use Drupal\KernelTests\KernelTestBase;
11 * Tests the menu tree storage.
15 * @see \Drupal\Core\Menu\MenuTreeStorage
17 class MenuTreeStorageTest extends KernelTestBase {
20 * The tested tree storage.
22 * @var \Drupal\Core\Menu\MenuTreeStorage
24 protected $treeStorage;
27 * The database connection.
29 * @var \Drupal\Core\Database\Connection
31 protected $connection;
36 protected function setUp() {
39 $this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), $this->container->get('cache_tags.invalidator'), 'menu_tree');
40 $this->connection = $this->container->get('database');
44 * Tests the tree storage when no tree was built yet.
46 public function testBasicMethods() {
47 $this->doTestEmptyStorage();
52 * Ensures that there are no menu links by default.
54 protected function doTestEmptyStorage() {
55 $this->assertEqual(0, $this->treeStorage->countMenuLinks());
59 * Ensures that table gets created on the fly.
61 protected function doTestTable() {
62 // Test that we can create a tree storage with an arbitrary table name and
63 // that selecting from the storage creates the table.
64 $tree_storage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), $this->container->get('cache_tags.invalidator'), 'test_menu_tree');
65 $this->assertFalse($this->connection->schema()->tableExists('test_menu_tree'), 'Test table is not yet created');
66 $tree_storage->countMenuLinks();
67 $this->assertTrue($this->connection->schema()->tableExists('test_menu_tree'), 'Test table was created');
71 * Tests with a simple linear hierarchy.
73 public function testSimpleHierarchy() {
74 // Add some links with parent on the previous one and test some values.
79 $this->addMenuLink('test1', '');
80 $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
82 $this->addMenuLink('test2', 'test1');
83 $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test2']);
84 $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 2], ['test1']);
86 $this->addMenuLink('test3', 'test2');
87 $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test2', 'test3']);
88 $this->assertMenuLink('test2', ['has_children' => 1, 'depth' => 2], ['test1'], ['test3']);
89 $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 3], ['test2', 'test1']);
93 * Tests the tree with moving links inside the hierarchy.
95 public function testMenuLinkMoving() {
105 $this->addMenuLink('test1', '');
106 $this->addMenuLink('test2', 'test1');
107 $this->addMenuLink('test3', 'test2');
108 $this->addMenuLink('test4', '');
109 $this->addMenuLink('test5', 'test4');
110 $this->addMenuLink('test6', 'test5');
112 $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test2', 'test3']);
113 $this->assertMenuLink('test2', ['has_children' => 1, 'depth' => 2], ['test1'], ['test3']);
114 $this->assertMenuLink('test4', ['has_children' => 1, 'depth' => 1], [], ['test5', 'test6']);
115 $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 2], ['test4'], ['test6']);
116 $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 3], ['test5', 'test4']);
118 $this->moveMenuLink('test2', 'test5');
119 // After the 1st move.
128 $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
129 $this->assertMenuLink('test2', ['has_children' => 1, 'depth' => 3], ['test5', 'test4'], ['test3']);
130 $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 4], ['test2', 'test5', 'test4']);
131 $this->assertMenuLink('test4', ['has_children' => 1, 'depth' => 1], [], ['test5', 'test2', 'test3', 'test6']);
132 $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 2], ['test4'], ['test2', 'test3', 'test6']);
133 $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 3], ['test5', 'test4']);
135 $this->moveMenuLink('test4', 'test1');
136 $this->moveMenuLink('test3', 'test1');
137 // After the next 2 moves.
146 $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test4', 'test5', 'test2', 'test3', 'test6']);
147 $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 4], ['test5', 'test4', 'test1']);
148 $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 2], ['test1']);
149 $this->assertMenuLink('test4', ['has_children' => 1, 'depth' => 2], ['test1'], ['test2', 'test5', 'test6']);
150 $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 3], ['test4', 'test1'], ['test2', 'test6']);
151 $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 4], ['test5', 'test4', 'test1']);
153 // Deleting a link in the middle should re-attach child links to the parent.
154 $this->treeStorage->delete('test4');
162 $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], ['test5', 'test2', 'test3', 'test6']);
163 $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 3], ['test5', 'test1']);
164 $this->assertMenuLink('test3', ['has_children' => 0, 'depth' => 2], ['test1']);
165 $this->assertFalse($this->treeStorage->load('test4'));
166 $this->assertMenuLink('test5', ['has_children' => 1, 'depth' => 2], ['test1'], ['test2', 'test6']);
167 $this->assertMenuLink('test6', ['has_children' => 0, 'depth' => 3], ['test5', 'test1']);
171 * Tests with disabled child links.
173 public function testMenuDisabledChildLinks() {
174 // Add some links with parent on the previous one and test some values.
177 // -- test2 (disabled)
179 $this->addMenuLink('test1', '');
180 $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
182 $this->addMenuLink('test2', 'test1', '<front>', [], 'tools', ['enabled' => 0]);
183 // The 1st link does not have any visible children, so has_children is 0.
184 $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1]);
185 $this->assertMenuLink('test2', ['has_children' => 0, 'depth' => 2, 'enabled' => 0], ['test1']);
187 // Add more links with parent on the previous one.
193 // -- test2 (disabled)
201 $this->addMenuLink('footerA', '', '<front>', [], 'footer');
202 $visible_children = [];
203 for ($i = 3; $i <= $this->treeStorage->maxDepth(); $i++) {
205 $this->addMenuLink("test$i", "test$parent");
206 $visible_children[] = "test$i";
208 // The 1st link does not have any visible children, so has_children is still
209 // 0. However, it has visible links below it that will be found.
210 $this->assertMenuLink('test1', ['has_children' => 0, 'depth' => 1], [], $visible_children);
211 // This should fail since test9 would end up at greater than max depth.
213 $this->moveMenuLink('test1', 'footerA');
214 $this->fail('Exception was not thrown');
216 catch (PluginException $e) {
217 $this->pass($e->getMessage());
219 // The opposite move should work, and change the has_children flag.
220 $this->moveMenuLink('footerA', 'test1');
221 $visible_children[] = 'footerA';
222 $this->assertMenuLink('test1', ['has_children' => 1, 'depth' => 1], [], $visible_children);
226 * Tests the loadTreeData method.
228 public function testLoadTree() {
229 $this->addMenuLink('test1', '');
230 $this->addMenuLink('test2', 'test1');
231 $this->addMenuLink('test3', 'test2');
232 $this->addMenuLink('test4');
233 $this->addMenuLink('test5', 'test4');
235 $data = $this->treeStorage->loadTreeData('tools', new MenuTreeParameters());
236 $tree = $data['tree'];
237 $this->assertEqual(count($tree['test1']['subtree']), 1);
238 $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']), 1);
239 $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']['test3']['subtree']), 0);
240 $this->assertEqual(count($tree['test4']['subtree']), 1);
241 $this->assertEqual(count($tree['test4']['subtree']['test5']['subtree']), 0);
243 $parameters = new MenuTreeParameters();
244 $parameters->setActiveTrail(['test4', 'test5']);
245 $data = $this->treeStorage->loadTreeData('tools', $parameters);
246 $tree = $data['tree'];
247 $this->assertEqual(count($tree['test1']['subtree']), 1);
248 $this->assertFalse($tree['test1']['in_active_trail']);
249 $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']), 1);
250 $this->assertFalse($tree['test1']['subtree']['test2']['in_active_trail']);
251 $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']['test3']['subtree']), 0);
252 $this->assertFalse($tree['test1']['subtree']['test2']['subtree']['test3']['in_active_trail']);
253 $this->assertEqual(count($tree['test4']['subtree']), 1);
254 $this->assertTrue($tree['test4']['in_active_trail']);
255 $this->assertEqual(count($tree['test4']['subtree']['test5']['subtree']), 0);
256 $this->assertTrue($tree['test4']['subtree']['test5']['in_active_trail']);
258 // Add some conditions to ensure that conditions work as expected.
259 $parameters = new MenuTreeParameters();
260 $parameters->addCondition('parent', 'test1');
261 $data = $this->treeStorage->loadTreeData('tools', $parameters);
262 $this->assertEqual(count($data['tree']), 1);
263 $this->assertEqual($data['tree']['test2']['definition']['id'], 'test2');
264 $this->assertEqual($data['tree']['test2']['subtree'], []);
266 // Test for only enabled links.
267 $link = $this->treeStorage->load('test3');
268 $link['enabled'] = FALSE;
269 $this->treeStorage->save($link);
270 $link = $this->treeStorage->load('test4');
271 $link['enabled'] = FALSE;
272 $this->treeStorage->save($link);
273 $link = $this->treeStorage->load('test5');
274 $link['enabled'] = FALSE;
275 $this->treeStorage->save($link);
277 $parameters = new MenuTreeParameters();
278 $parameters->onlyEnabledLinks();
279 $data = $this->treeStorage->loadTreeData('tools', $parameters);
280 $this->assertEqual(count($data['tree']), 1);
281 $this->assertEqual($data['tree']['test1']['definition']['id'], 'test1');
282 $this->assertEqual(count($data['tree']['test1']['subtree']), 1);
283 $this->assertEqual($data['tree']['test1']['subtree']['test2']['definition']['id'], 'test2');
284 $this->assertEqual($data['tree']['test1']['subtree']['test2']['subtree'], []);
289 * Tests finding the subtree height with content menu links.
291 public function testSubtreeHeight() {
297 $this->addMenuLink('root');
298 $this->addMenuLink('child1', 'root');
299 $this->addMenuLink('child2', 'child1');
300 $this->addMenuLink('child3', 'child2');
301 $this->addMenuLink('child4', 'child3');
303 $this->assertEqual($this->treeStorage->getSubtreeHeight('root'), 5);
304 $this->assertEqual($this->treeStorage->getSubtreeHeight('child1'), 4);
305 $this->assertEqual($this->treeStorage->getSubtreeHeight('child2'), 3);
306 $this->assertEqual($this->treeStorage->getSubtreeHeight('child3'), 2);
307 $this->assertEqual($this->treeStorage->getSubtreeHeight('child4'), 1);
311 * Ensure hierarchy persists after a menu rebuild.
313 public function testMenuRebuild() {
319 $this->addMenuLink('root');
320 $this->addMenuLink('child1', 'root');
321 $this->addMenuLink('child2', 'child1');
322 $this->addMenuLink('child3', 'child2');
323 $this->addMenuLink('child4', 'child3');
325 $this->assertEqual($this->treeStorage->getSubtreeHeight('root'), 5);
326 $this->assertEqual($this->treeStorage->getSubtreeHeight('child1'), 4);
327 $this->assertEqual($this->treeStorage->getSubtreeHeight('child2'), 3);
328 $this->assertEqual($this->treeStorage->getSubtreeHeight('child3'), 2);
329 $this->assertEqual($this->treeStorage->getSubtreeHeight('child4'), 1);
331 // Intentionally leave child3 out to mimic static or external links.
332 $definitions = $this->treeStorage->loadMultiple(['root', 'child1', 'child2', 'child4']);
333 $this->treeStorage->rebuild($definitions);
334 $this->assertEqual($this->treeStorage->getSubtreeHeight('root'), 5);
335 $this->assertEqual($this->treeStorage->getSubtreeHeight('child1'), 4);
336 $this->assertEqual($this->treeStorage->getSubtreeHeight('child2'), 3);
337 $this->assertEqual($this->treeStorage->getSubtreeHeight('child3'), 2);
338 $this->assertEqual($this->treeStorage->getSubtreeHeight('child4'), 1);
342 * Tests MenuTreeStorage::loadByProperties().
344 public function testLoadByProperties() {
349 $message = 'An invalid property name throws an exception.';
350 foreach ($tests as $properties) {
352 $this->treeStorage->loadByProperties($properties);
353 $this->fail($message);
355 catch (\InvalidArgumentException $e) {
356 $this->assertTrue(preg_match('/^An invalid property name, .+ was specified. Allowed property names are:/', $e->getMessage()), 'Found expected exception message.');
357 $this->pass($message);
360 $this->addMenuLink('test_link.1', '', 'test', [], 'menu1');
361 $properties = ['menu_name' => 'menu1'];
362 $links = $this->treeStorage->loadByProperties($properties);
363 $this->assertEqual('menu1', $links['test_link.1']['menu_name']);
364 $this->assertEqual('test', $links['test_link.1']['route_name']);
368 * Adds a link with the given ID and supply defaults.
370 protected function addMenuLink($id, $parent = '', $route_name = 'test', $route_parameters = [], $menu_name = 'tools', $extra = []) {
373 'menu_name' => $menu_name,
374 'route_name' => $route_name,
375 'route_parameters' => $route_parameters,
381 $this->treeStorage->save($link);
385 * Moves the link with the given ID so it's under a new parent.
388 * The ID of the menu link to move.
389 * @param string $new_parent
390 * The ID of the new parent link.
392 protected function moveMenuLink($id, $new_parent) {
393 $menu_link = $this->treeStorage->load($id);
394 $menu_link['parent'] = $new_parent;
395 $this->treeStorage->save($menu_link);
399 * Tests that a link's stored representation matches the expected values.
402 * The ID of the menu link to test
403 * @param array $expected_properties
404 * A keyed array of column names and values like has_children and depth.
405 * @param array $parents
406 * An ordered array of the IDs of the menu links that are the parents.
407 * @param array $children
408 * Array of child IDs that are visible (enabled == 1).
410 protected function assertMenuLink($id, array $expected_properties, array $parents = [], array $children = []) {
411 $query = $this->connection->select('menu_tree');
412 $query->fields('menu_tree');
413 $query->condition('id', $id);
414 foreach ($expected_properties as $field => $value) {
415 $query->condition($field, $value);
417 $all = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
418 $this->assertEqual(count($all), 1, "Found link $id matching all the expected properties");
421 // Put the current link onto the front.
422 array_unshift($parents, $raw['id']);
424 $query = $this->connection->select('menu_tree');
425 $query->fields('menu_tree', ['id', 'mlid']);
426 $query->condition('id', $parents, 'IN');
427 $found_parents = $query->execute()->fetchAllKeyed(0, 1);
429 $this->assertEqual(count($parents), count($found_parents), 'Found expected number of parents');
430 $this->assertEqual($raw['depth'], count($found_parents), 'Number of parents is the same as the depth');
432 $materialized_path = $this->treeStorage->getRootPathIds($id);
433 $this->assertEqual(array_values($materialized_path), array_values($parents), 'Parents match the materialized path');
434 // Check that the selected mlid values of the parents are in the correct
435 // column, including the link's own.
436 for ($i = $raw['depth']; $i >= 1; $i--) {
437 $parent_id = array_shift($parents);
438 $this->assertEqual($raw["p$i"], $found_parents[$parent_id], "mlid of parent matches at column p$i");
440 for ($i = $raw['depth'] + 1; $i <= $this->treeStorage->maxDepth(); $i++) {
441 $this->assertEqual($raw["p$i"], 0, "parent is 0 at column p$i greater than depth");
444 $this->assertEqual($raw['parent'], end($parents), 'Ensure that the parent field is set properly');
446 $found_children = array_keys($this->treeStorage->loadAllChildren($id));
447 // We need both these checks since the 2nd will pass if there are extra
448 // IDs loaded in $found_children.
449 $this->assertEqual(count($children), count($found_children), "Found expected number of children for $id");
450 $this->assertEqual(array_intersect($children, $found_children), $children, 'Child IDs match');