3 namespace Drupal\Tests\Core\Menu;
5 use Drupal\Core\Access\AccessResult;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Cache\CacheableDependencyInterface;
8 use Drupal\Core\Cache\CacheableMetadata;
9 use Drupal\Core\Cache\Context\CacheContextsManager;
10 use Drupal\Core\Controller\ControllerResolver;
11 use Drupal\Core\DependencyInjection\ContainerBuilder;
12 use Drupal\Core\Language\Language;
13 use Drupal\Core\Menu\LocalTaskInterface;
14 use Drupal\Core\Menu\LocalTaskManager;
15 use Drupal\Tests\UnitTestCase;
16 use Prophecy\Argument;
17 use Symfony\Component\HttpFoundation\ParameterBag;
18 use Symfony\Component\HttpFoundation\Request;
19 use Symfony\Component\HttpFoundation\RequestStack;
22 * @coversDefaultClass \Drupal\Core\Menu\LocalTaskManager
25 class LocalTaskManagerTest extends UnitTestCase {
30 * @var \Drupal\Core\Menu\LocalTaskManager
35 * The mocked argument resolver.
37 * @var \PHPUnit_Framework_MockObject_MockObject
39 protected $argumentResolver;
44 * @var \Symfony\Component\HttpFoundation\Request
49 * The mocked route provider.
51 * @var \PHPUnit_Framework_MockObject_MockObject
53 protected $routeProvider;
56 * The mocked plugin discovery.
58 * @var \PHPUnit_Framework_MockObject_MockObject
60 protected $pluginDiscovery;
63 * The plugin factory used in the test.
65 * @var \PHPUnit_Framework_MockObject_MockObject
70 * The cache backend used in the test.
72 * @var \PHPUnit_Framework_MockObject_MockObject
74 protected $cacheBackend;
77 * The mocked access manager.
79 * @var \Drupal\Core\Access\AccessManagerInterface|\PHPUnit_Framework_MockObject_MockObject
81 protected $accessManager;
86 * @var \Drupal\Core\Routing\RouteMatchInterface|\PHPUnit_Framework_MockObject_MockObject
88 protected $routeMatch;
93 * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
100 protected function setUp() {
103 $this->argumentResolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface');
104 $this->request = new Request();
105 $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
106 $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface');
107 $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface');
108 $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
109 $this->accessManager = $this->getMock('Drupal\Core\Access\AccessManagerInterface');
110 $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
111 $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
113 $this->setupLocalTaskManager();
114 $this->setupNullCacheabilityMetadataValidation();
118 * Tests the getLocalTasksForRoute method.
120 * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getLocalTasksForRoute()
122 public function testGetLocalTasksForRouteSingleLevelTitle() {
123 $definitions = $this->getLocalTaskFixtures();
125 $this->pluginDiscovery->expects($this->once())
126 ->method('getDefinitions')
127 ->will($this->returnValue($definitions));
129 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
131 $this->setupFactory($mock_plugin);
132 $this->setupLocalTaskManager();
134 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view');
136 $result = $this->getLocalTasksForRouteResult($mock_plugin);
138 $this->assertEquals($result, $local_tasks);
142 * Tests the getLocalTasksForRoute method on a child.
144 * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getLocalTasksForRoute()
146 public function testGetLocalTasksForRouteForChild() {
147 $definitions = $this->getLocalTaskFixtures();
149 $this->pluginDiscovery->expects($this->once())
150 ->method('getDefinitions')
151 ->will($this->returnValue($definitions));
153 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
155 $this->setupFactory($mock_plugin);
156 $this->setupLocalTaskManager();
158 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_child1_page');
160 $result = $this->getLocalTasksForRouteResult($mock_plugin);
162 $this->assertEquals($result, $local_tasks);
166 * Tests the cache of the local task manager with an empty initial cache.
168 public function testGetLocalTaskForRouteWithEmptyCache() {
169 $definitions = $this->getLocalTaskFixtures();
171 $this->pluginDiscovery->expects($this->once())
172 ->method('getDefinitions')
173 ->will($this->returnValue($definitions));
175 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
176 $this->setupFactory($mock_plugin);
178 $this->setupLocalTaskManager();
180 $result = $this->getLocalTasksForRouteResult($mock_plugin);
182 $this->cacheBackend->expects($this->at(0))
184 ->with('local_task_plugins:en:menu_local_task_test_tasks_view');
186 $this->cacheBackend->expects($this->at(1))
188 ->with('local_task_plugins:en');
190 $this->cacheBackend->expects($this->at(2))
192 ->with('local_task_plugins:en', $definitions, Cache::PERMANENT);
194 $expected_set = $this->getLocalTasksCache();
196 $this->cacheBackend->expects($this->at(3))
198 ->with('local_task_plugins:en:menu_local_task_test_tasks_view', $expected_set, Cache::PERMANENT, ['local_task']);
200 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view');
201 $this->assertEquals($result, $local_tasks);
205 * Tests the cache of the local task manager with a filled initial cache.
207 public function testGetLocalTaskForRouteWithFilledCache() {
208 $this->pluginDiscovery->expects($this->never())
209 ->method('getDefinitions');
211 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
212 $this->setupFactory($mock_plugin);
214 $this->setupLocalTaskManager();
216 $result = $this->getLocalTasksCache($mock_plugin);
218 $this->cacheBackend->expects($this->at(0))
220 ->with('local_task_plugins:en:menu_local_task_test_tasks_view')
221 ->will($this->returnValue((object) ['data' => $result]));
223 $this->cacheBackend->expects($this->never())
226 $result = $this->getLocalTasksForRouteResult($mock_plugin);
227 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view');
228 $this->assertEquals($result, $local_tasks);
232 * Tests the getTitle method.
234 * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getTitle()
236 public function testGetTitle() {
237 $menu_local_task = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
238 $menu_local_task->expects($this->once())
239 ->method('getTitle');
241 $this->argumentResolver->expects($this->once())
242 ->method('getArguments')
243 ->with($this->request, [$menu_local_task, 'getTitle'])
244 ->will($this->returnValue([]));
246 $this->manager->getTitle($menu_local_task);
250 * @expectedDeprecation Using the 'controller_resolver' service as the first argument is deprecated, use the 'http_kernel.controller.argument_resolver' instead. If your subclass requires the 'controller_resolver' service add it as an additional argument. See https://www.drupal.org/node/2959408.
253 public function testControllerResolverDeprecation() {
254 $controller_resolver = $this->getMockBuilder(ControllerResolver::class)->disableOriginalConstructor()->getMock();
255 $request_stack = new RequestStack();
256 $request_stack->push($this->request);
257 $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
258 $module_handler->expects($this->any())
259 ->method('getModuleDirectories')
261 $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
262 $language_manager->expects($this->any())
263 ->method('getCurrentLanguage')
264 ->will($this->returnValue(new Language(['id' => 'en'])));
266 new LocalTaskManager($controller_resolver, $request_stack, $this->routeMatch, $this->routeProvider, $module_handler, $this->cacheBackend, $language_manager, $this->accessManager, $this->account);
270 * Setups the local task manager for the test.
272 protected function setupLocalTaskManager() {
273 $request_stack = new RequestStack();
274 $request_stack->push($this->request);
275 $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
276 $module_handler->expects($this->any())
277 ->method('getModuleDirectories')
279 $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
280 $language_manager->expects($this->any())
281 ->method('getCurrentLanguage')
282 ->will($this->returnValue(new Language(['id' => 'en'])));
284 $this->manager = new LocalTaskManager($this->argumentResolver, $request_stack, $this->routeMatch, $this->routeProvider, $module_handler, $this->cacheBackend, $language_manager, $this->accessManager, $this->account);
286 $property = new \ReflectionProperty('Drupal\Core\Menu\LocalTaskManager', 'discovery');
287 $property->setAccessible(TRUE);
288 $property->setValue($this->manager, $this->pluginDiscovery);
290 $property = new \ReflectionProperty('Drupal\Core\Menu\LocalTaskManager', 'factory');
291 $property->setAccessible(TRUE);
292 $property->setValue($this->manager, $this->factory);
297 * Return some local tasks plugin definitions.
300 * An array of plugin definition keyed by plugin ID.
302 protected function getLocalTaskFixtures() {
304 $definitions['menu_local_task_test_tasks_settings'] = [
305 'route_name' => 'menu_local_task_test_tasks_settings',
306 'title' => 'Settings',
307 'base_route' => 'menu_local_task_test_tasks_view',
309 $definitions['menu_local_task_test_tasks_edit'] = [
310 'route_name' => 'menu_local_task_test_tasks_edit',
311 'title' => 'Settings',
312 'base_route' => 'menu_local_task_test_tasks_view',
315 // Make this ID different from the route name to catch code that
317 $definitions['menu_local_task_test_tasks_view.tab'] = [
318 'route_name' => 'menu_local_task_test_tasks_view',
319 'title' => 'Settings',
320 'base_route' => 'menu_local_task_test_tasks_view',
323 $definitions['menu_local_task_test_tasks_view_child1'] = [
324 'route_name' => 'menu_local_task_test_tasks_child1_page',
325 'title' => 'Settings child #1',
326 'parent_id' => 'menu_local_task_test_tasks_view.tab',
328 $definitions['menu_local_task_test_tasks_view_child2'] = [
329 'route_name' => 'menu_local_task_test_tasks_child2_page',
330 'title' => 'Settings child #2',
331 'parent_id' => 'menu_local_task_test_tasks_view.tab',
332 'base_route' => 'this_should_be_replaced',
334 // Add the ID and defaults from the LocalTaskManager.
335 foreach ($definitions as $id => &$info) {
340 'route_parameters' => [],
346 'class' => 'Drupal\Core\Menu\LocalTaskDefault',
353 * Setups the plugin factory with some local task plugins.
355 * @param \PHPUnit_Framework_MockObject_MockObject $mock_plugin
358 protected function setupFactory($mock_plugin) {
360 foreach ($this->getLocalTaskFixtures() as $info) {
361 $map[] = [$info['id'], [], $mock_plugin];
363 $this->factory->expects($this->any())
364 ->method('createInstance')
365 ->will($this->returnValueMap($map));
369 * Returns an expected result for getLocalTasksForRoute.
371 * @param \PHPUnit_Framework_MockObject_MockObject $mock_plugin
375 * The expected result, keyed by local task level.
377 protected function getLocalTasksForRouteResult($mock_plugin) {
380 'menu_local_task_test_tasks_settings' => $mock_plugin,
381 'menu_local_task_test_tasks_view.tab' => $mock_plugin,
382 'menu_local_task_test_tasks_edit' => $mock_plugin,
385 'menu_local_task_test_tasks_view_child1' => $mock_plugin,
386 'menu_local_task_test_tasks_view_child2' => $mock_plugin,
393 * Returns the cache entry expected when running getLocalTaskForRoute().
397 protected function getLocalTasksCache() {
398 $local_task_fixtures = $this->getLocalTaskFixtures();
401 'menu_local_task_test_tasks_view' => 'menu_local_task_test_tasks_view',
404 'menu_local_task_test_tasks_view.tab' => TRUE,
407 '> menu_local_task_test_tasks_view' => [
408 'menu_local_task_test_tasks_settings' => $local_task_fixtures['menu_local_task_test_tasks_settings'],
409 'menu_local_task_test_tasks_edit' => $local_task_fixtures['menu_local_task_test_tasks_edit'],
410 'menu_local_task_test_tasks_view.tab' => $local_task_fixtures['menu_local_task_test_tasks_view.tab'],
412 'menu_local_task_test_tasks_view.tab' => [
413 // The manager will fill in the base_route before caching.
414 'menu_local_task_test_tasks_view_child1' => ['base_route' => 'menu_local_task_test_tasks_view'] + $local_task_fixtures['menu_local_task_test_tasks_view_child1'],
415 'menu_local_task_test_tasks_view_child2' => ['base_route' => 'menu_local_task_test_tasks_view'] + $local_task_fixtures['menu_local_task_test_tasks_view_child2'],
419 $local_tasks['children']['> menu_local_task_test_tasks_view']['menu_local_task_test_tasks_settings']['weight'] = 0;
420 $local_tasks['children']['> menu_local_task_test_tasks_view']['menu_local_task_test_tasks_edit']['weight'] = 20 + 1e-6;
421 $local_tasks['children']['> menu_local_task_test_tasks_view']['menu_local_task_test_tasks_view.tab']['weight'] = 2e-6;
422 $local_tasks['children']['menu_local_task_test_tasks_view.tab']['menu_local_task_test_tasks_view_child1']['weight'] = 3e-6;
423 $local_tasks['children']['menu_local_task_test_tasks_view.tab']['menu_local_task_test_tasks_view_child2']['weight'] = 4e-6;
428 * @covers ::getTasksBuild
430 public function testGetTasksBuildWithCacheabilityMetadata() {
431 $definitions = $this->getLocalTaskFixtures();
433 $this->pluginDiscovery->expects($this->once())
434 ->method('getDefinitions')
435 ->will($this->returnValue($definitions));
437 // Set up some cacheability metadata and ensure its merged together.
438 $definitions['menu_local_task_test_tasks_settings']['cache_tags'] = ['tag.example1'];
439 $definitions['menu_local_task_test_tasks_settings']['cache_contexts'] = ['context.example1'];
440 $definitions['menu_local_task_test_tasks_edit']['cache_tags'] = ['tag.example2'];
441 $definitions['menu_local_task_test_tasks_edit']['cache_contexts'] = ['context.example2'];
442 // Test the cacheability metadata of access checking.
443 $definitions['menu_local_task_test_tasks_view_child1']['access'] = AccessResult::allowed()->addCacheContexts(['user.permissions']);
445 $this->setupFactoryAndLocalTaskPlugins($definitions, 'menu_local_task_test_tasks_view');
446 $this->setupLocalTaskManager();
448 $this->argumentResolver->expects($this->any())
449 ->method('getArguments')
452 $this->routeMatch->expects($this->any())
453 ->method('getRouteName')
454 ->willReturn('menu_local_task_test_tasks_view');
455 $this->routeMatch->expects($this->any())
456 ->method('getRawParameters')
457 ->willReturn(new ParameterBag());
459 $cacheability = new CacheableMetadata();
460 $local_tasks = $this->manager->getTasksBuild('menu_local_task_test_tasks_view', $cacheability);
462 // Ensure that all cacheability metadata is merged together.
463 $this->assertEquals(['tag.example1', 'tag.example2'], $cacheability->getCacheTags());
464 $this->assertEquals(['context.example1', 'context.example2', 'route', 'user.permissions'], $cacheability->getCacheContexts());
467 protected function setupFactoryAndLocalTaskPlugins(array $definitions, $active_plugin_id) {
469 $access_manager_map = [];
471 foreach ($definitions as $plugin_id => $info) {
472 $info += ['access' => AccessResult::allowed()];
474 $mock = $this->prophesize(LocalTaskInterface::class);
475 $mock->willImplement(CacheableDependencyInterface::class);
476 $mock->getRouteName()->willReturn($info['route_name']);
477 $mock->getTitle()->willReturn($info['title']);
478 $mock->getRouteParameters(Argument::cetera())->willReturn([]);
479 $mock->getOptions(Argument::cetera())->willReturn([]);
480 $mock->getActive()->willReturn($plugin_id === $active_plugin_id);
481 $mock->getWeight()->willReturn(isset($info['weight']) ? $info['weight'] : 0);
482 $mock->getCacheContexts()->willReturn(isset($info['cache_contexts']) ? $info['cache_contexts'] : []);
483 $mock->getCacheTags()->willReturn(isset($info['cache_tags']) ? $info['cache_tags'] : []);
484 $mock->getCacheMaxAge()->willReturn(isset($info['cache_max_age']) ? $info['cache_max_age'] : Cache::PERMANENT);
486 $access_manager_map[] = [$info['route_name'], [], $this->account, TRUE, $info['access']];
488 $map[] = [$info['id'], [], $mock->reveal()];
491 $this->accessManager->expects($this->any())
492 ->method('checkNamedRoute')
493 ->willReturnMap($access_manager_map);
495 $this->factory->expects($this->any())
496 ->method('createInstance')
497 ->will($this->returnValueMap($map));
500 protected function setupNullCacheabilityMetadataValidation() {
501 $container = \Drupal::hasContainer() ? \Drupal::getContainer() : new ContainerBuilder();
503 $cache_context_manager = $this->prophesize(CacheContextsManager::class);
505 foreach ([NULL, ['user.permissions'], ['route'], ['route', 'context.example1'], ['context.example1', 'route'], ['context.example1', 'route', 'context.example2'], ['context.example1', 'context.example2', 'route'], ['context.example1', 'context.example2', 'route', 'user.permissions']] as $argument) {
506 $cache_context_manager->assertValidTokens($argument)->willReturn(TRUE);
509 $container->set('cache_contexts_manager', $cache_context_manager->reveal());
510 \Drupal::setContainer($container);