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\DependencyInjection\ContainerBuilder;
11 use Drupal\Core\Language\Language;
12 use Drupal\Core\Menu\LocalTaskInterface;
13 use Drupal\Core\Menu\LocalTaskManager;
14 use Drupal\Tests\UnitTestCase;
15 use Prophecy\Argument;
16 use Symfony\Component\HttpFoundation\ParameterBag;
17 use Symfony\Component\HttpFoundation\Request;
18 use Symfony\Component\HttpFoundation\RequestStack;
21 * @coversDefaultClass \Drupal\Core\Menu\LocalTaskManager
24 class LocalTaskManagerTest extends UnitTestCase {
29 * @var \Drupal\Core\Menu\LocalTaskManager
34 * The mocked controller resolver.
36 * @var \PHPUnit_Framework_MockObject_MockObject
38 protected $controllerResolver;
43 * @var \Symfony\Component\HttpFoundation\Request
48 * The mocked route provider.
50 * @var \PHPUnit_Framework_MockObject_MockObject
52 protected $routeProvider;
55 * The mocked plugin discovery.
57 * @var \PHPUnit_Framework_MockObject_MockObject
59 protected $pluginDiscovery;
62 * The plugin factory used in the test.
64 * @var \PHPUnit_Framework_MockObject_MockObject
69 * The cache backend used in the test.
71 * @var \PHPUnit_Framework_MockObject_MockObject
73 protected $cacheBackend;
76 * The mocked access manager.
78 * @var \Drupal\Core\Access\AccessManagerInterface|\PHPUnit_Framework_MockObject_MockObject
80 protected $accessManager;
85 * @var \Drupal\Core\Routing\RouteMatchInterface|\PHPUnit_Framework_MockObject_MockObject
87 protected $routeMatch;
92 * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
99 protected function setUp() {
102 $this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
103 $this->request = new Request();
104 $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
105 $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface');
106 $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface');
107 $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
108 $this->accessManager = $this->getMock('Drupal\Core\Access\AccessManagerInterface');
109 $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
110 $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
112 $this->setupLocalTaskManager();
113 $this->setupNullCacheabilityMetadataValidation();
117 * Tests the getLocalTasksForRoute method.
119 * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getLocalTasksForRoute()
121 public function testGetLocalTasksForRouteSingleLevelTitle() {
122 $definitions = $this->getLocalTaskFixtures();
124 $this->pluginDiscovery->expects($this->once())
125 ->method('getDefinitions')
126 ->will($this->returnValue($definitions));
128 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
130 $this->setupFactory($mock_plugin);
131 $this->setupLocalTaskManager();
133 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view');
135 $result = $this->getLocalTasksForRouteResult($mock_plugin);
137 $this->assertEquals($result, $local_tasks);
141 * Tests the getLocalTasksForRoute method on a child.
143 * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getLocalTasksForRoute()
145 public function testGetLocalTasksForRouteForChild() {
146 $definitions = $this->getLocalTaskFixtures();
148 $this->pluginDiscovery->expects($this->once())
149 ->method('getDefinitions')
150 ->will($this->returnValue($definitions));
152 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
154 $this->setupFactory($mock_plugin);
155 $this->setupLocalTaskManager();
157 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_child1_page');
159 $result = $this->getLocalTasksForRouteResult($mock_plugin);
161 $this->assertEquals($result, $local_tasks);
165 * Tests the cache of the local task manager with an empty initial cache.
167 public function testGetLocalTaskForRouteWithEmptyCache() {
168 $definitions = $this->getLocalTaskFixtures();
170 $this->pluginDiscovery->expects($this->once())
171 ->method('getDefinitions')
172 ->will($this->returnValue($definitions));
174 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
175 $this->setupFactory($mock_plugin);
177 $this->setupLocalTaskManager();
179 $result = $this->getLocalTasksForRouteResult($mock_plugin);
181 $this->cacheBackend->expects($this->at(0))
183 ->with('local_task_plugins:en:menu_local_task_test_tasks_view');
185 $this->cacheBackend->expects($this->at(1))
187 ->with('local_task_plugins:en');
189 $this->cacheBackend->expects($this->at(2))
191 ->with('local_task_plugins:en', $definitions, Cache::PERMANENT);
193 $expected_set = $this->getLocalTasksCache();
195 $this->cacheBackend->expects($this->at(3))
197 ->with('local_task_plugins:en:menu_local_task_test_tasks_view', $expected_set, Cache::PERMANENT, ['local_task']);
199 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view');
200 $this->assertEquals($result, $local_tasks);
204 * Tests the cache of the local task manager with a filled initial cache.
206 public function testGetLocalTaskForRouteWithFilledCache() {
207 $this->pluginDiscovery->expects($this->never())
208 ->method('getDefinitions');
210 $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
211 $this->setupFactory($mock_plugin);
213 $this->setupLocalTaskManager();
215 $result = $this->getLocalTasksCache($mock_plugin);
217 $this->cacheBackend->expects($this->at(0))
219 ->with('local_task_plugins:en:menu_local_task_test_tasks_view')
220 ->will($this->returnValue((object) ['data' => $result]));
222 $this->cacheBackend->expects($this->never())
225 $result = $this->getLocalTasksForRouteResult($mock_plugin);
226 $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view');
227 $this->assertEquals($result, $local_tasks);
231 * Tests the getTitle method.
233 * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getTitle()
235 public function testGetTitle() {
236 $menu_local_task = $this->getMock('Drupal\Core\Menu\LocalTaskInterface');
237 $menu_local_task->expects($this->once())
238 ->method('getTitle');
240 $this->controllerResolver->expects($this->once())
241 ->method('getArguments')
242 ->with($this->request, [$menu_local_task, 'getTitle'])
243 ->will($this->returnValue([]));
245 $this->manager->getTitle($menu_local_task);
249 * Setups the local task manager for the test.
251 protected function setupLocalTaskManager() {
252 $request_stack = new RequestStack();
253 $request_stack->push($this->request);
254 $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
255 $module_handler->expects($this->any())
256 ->method('getModuleDirectories')
258 $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
259 $language_manager->expects($this->any())
260 ->method('getCurrentLanguage')
261 ->will($this->returnValue(new Language(['id' => 'en'])));
263 $this->manager = new LocalTaskManager($this->controllerResolver, $request_stack, $this->routeMatch, $this->routeProvider, $module_handler, $this->cacheBackend, $language_manager, $this->accessManager, $this->account);
265 $property = new \ReflectionProperty('Drupal\Core\Menu\LocalTaskManager', 'discovery');
266 $property->setAccessible(TRUE);
267 $property->setValue($this->manager, $this->pluginDiscovery);
269 $property = new \ReflectionProperty('Drupal\Core\Menu\LocalTaskManager', 'factory');
270 $property->setAccessible(TRUE);
271 $property->setValue($this->manager, $this->factory);
276 * Return some local tasks plugin definitions.
279 * An array of plugin definition keyed by plugin ID.
281 protected function getLocalTaskFixtures() {
283 $definitions['menu_local_task_test_tasks_settings'] = [
284 'route_name' => 'menu_local_task_test_tasks_settings',
285 'title' => 'Settings',
286 'base_route' => 'menu_local_task_test_tasks_view',
288 $definitions['menu_local_task_test_tasks_edit'] = [
289 'route_name' => 'menu_local_task_test_tasks_edit',
290 'title' => 'Settings',
291 'base_route' => 'menu_local_task_test_tasks_view',
294 // Make this ID different from the route name to catch code that
296 $definitions['menu_local_task_test_tasks_view.tab'] = [
297 'route_name' => 'menu_local_task_test_tasks_view',
298 'title' => 'Settings',
299 'base_route' => 'menu_local_task_test_tasks_view',
302 $definitions['menu_local_task_test_tasks_view_child1'] = [
303 'route_name' => 'menu_local_task_test_tasks_child1_page',
304 'title' => 'Settings child #1',
305 'parent_id' => 'menu_local_task_test_tasks_view.tab',
307 $definitions['menu_local_task_test_tasks_view_child2'] = [
308 'route_name' => 'menu_local_task_test_tasks_child2_page',
309 'title' => 'Settings child #2',
310 'parent_id' => 'menu_local_task_test_tasks_view.tab',
311 'base_route' => 'this_should_be_replaced',
313 // Add the ID and defaults from the LocalTaskManager.
314 foreach ($definitions as $id => &$info) {
319 'route_parameters' => [],
325 'class' => 'Drupal\Core\Menu\LocalTaskDefault',
332 * Setups the plugin factory with some local task plugins.
334 * @param \PHPUnit_Framework_MockObject_MockObject $mock_plugin
337 protected function setupFactory($mock_plugin) {
339 foreach ($this->getLocalTaskFixtures() as $info) {
340 $map[] = [$info['id'], [], $mock_plugin];
342 $this->factory->expects($this->any())
343 ->method('createInstance')
344 ->will($this->returnValueMap($map));
348 * Returns an expected result for getLocalTasksForRoute.
350 * @param \PHPUnit_Framework_MockObject_MockObject $mock_plugin
354 * The expected result, keyed by local task level.
356 protected function getLocalTasksForRouteResult($mock_plugin) {
359 'menu_local_task_test_tasks_settings' => $mock_plugin,
360 'menu_local_task_test_tasks_view.tab' => $mock_plugin,
361 'menu_local_task_test_tasks_edit' => $mock_plugin,
364 'menu_local_task_test_tasks_view_child1' => $mock_plugin,
365 'menu_local_task_test_tasks_view_child2' => $mock_plugin,
372 * Returns the cache entry expected when running getLocalTaskForRoute().
376 protected function getLocalTasksCache() {
377 $local_task_fixtures = $this->getLocalTaskFixtures();
380 'menu_local_task_test_tasks_view' => 'menu_local_task_test_tasks_view',
383 'menu_local_task_test_tasks_view.tab' => TRUE,
386 '> menu_local_task_test_tasks_view' => [
387 'menu_local_task_test_tasks_settings' => $local_task_fixtures['menu_local_task_test_tasks_settings'],
388 'menu_local_task_test_tasks_edit' => $local_task_fixtures['menu_local_task_test_tasks_edit'],
389 'menu_local_task_test_tasks_view.tab' => $local_task_fixtures['menu_local_task_test_tasks_view.tab'],
391 'menu_local_task_test_tasks_view.tab' => [
392 // The manager will fill in the base_route before caching.
393 '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'],
394 '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'],
398 $local_tasks['children']['> menu_local_task_test_tasks_view']['menu_local_task_test_tasks_settings']['weight'] = 0;
399 $local_tasks['children']['> menu_local_task_test_tasks_view']['menu_local_task_test_tasks_edit']['weight'] = 20 + 1e-6;
400 $local_tasks['children']['> menu_local_task_test_tasks_view']['menu_local_task_test_tasks_view.tab']['weight'] = 2e-6;
401 $local_tasks['children']['menu_local_task_test_tasks_view.tab']['menu_local_task_test_tasks_view_child1']['weight'] = 3e-6;
402 $local_tasks['children']['menu_local_task_test_tasks_view.tab']['menu_local_task_test_tasks_view_child2']['weight'] = 4e-6;
407 * @covers ::getTasksBuild
409 public function testGetTasksBuildWithCacheabilityMetadata() {
410 $definitions = $this->getLocalTaskFixtures();
412 $this->pluginDiscovery->expects($this->once())
413 ->method('getDefinitions')
414 ->will($this->returnValue($definitions));
416 // Set up some cacheablity metadata and ensure its merged together.
417 $definitions['menu_local_task_test_tasks_settings']['cache_tags'] = ['tag.example1'];
418 $definitions['menu_local_task_test_tasks_settings']['cache_contexts'] = ['context.example1'];
419 $definitions['menu_local_task_test_tasks_edit']['cache_tags'] = ['tag.example2'];
420 $definitions['menu_local_task_test_tasks_edit']['cache_contexts'] = ['context.example2'];
421 // Test the cacheability metadata of access checking.
422 $definitions['menu_local_task_test_tasks_view_child1']['access'] = AccessResult::allowed()->addCacheContexts(['user.permissions']);
424 $this->setupFactoryAndLocalTaskPlugins($definitions, 'menu_local_task_test_tasks_view');
425 $this->setupLocalTaskManager();
427 $this->controllerResolver->expects($this->any())
428 ->method('getArguments')
431 $this->routeMatch->expects($this->any())
432 ->method('getRouteName')
433 ->willReturn('menu_local_task_test_tasks_view');
434 $this->routeMatch->expects($this->any())
435 ->method('getRawParameters')
436 ->willReturn(new ParameterBag());
438 $cacheability = new CacheableMetadata();
439 $local_tasks = $this->manager->getTasksBuild('menu_local_task_test_tasks_view', $cacheability);
441 // Ensure that all cacheability metadata is merged together.
442 $this->assertEquals(['tag.example1', 'tag.example2'], $cacheability->getCacheTags());
443 $this->assertEquals(['context.example1', 'context.example2', 'route', 'user.permissions'], $cacheability->getCacheContexts());
446 protected function setupFactoryAndLocalTaskPlugins(array $definitions, $active_plugin_id) {
448 $access_manager_map = [];
450 foreach ($definitions as $plugin_id => $info) {
451 $info += ['access' => AccessResult::allowed()];
453 $mock = $this->prophesize(LocalTaskInterface::class);
454 $mock->willImplement(CacheableDependencyInterface::class);
455 $mock->getRouteName()->willReturn($info['route_name']);
456 $mock->getTitle()->willReturn($info['title']);
457 $mock->getRouteParameters(Argument::cetera())->willReturn([]);
458 $mock->getOptions(Argument::cetera())->willReturn([]);
459 $mock->getActive()->willReturn($plugin_id === $active_plugin_id);
460 $mock->getWeight()->willReturn(isset($info['weight']) ? $info['weight'] : 0);
461 $mock->getCacheContexts()->willReturn(isset($info['cache_contexts']) ? $info['cache_contexts'] : []);
462 $mock->getCacheTags()->willReturn(isset($info['cache_tags']) ? $info['cache_tags'] : []);
463 $mock->getCacheMaxAge()->willReturn(isset($info['cache_max_age']) ? $info['cache_max_age'] : Cache::PERMANENT);
466 $access_manager_map[] = [$info['route_name'], [], $this->account, TRUE, $info['access']];
468 $map[] = [$info['id'], [], $mock->reveal()];
471 $this->accessManager->expects($this->any())
472 ->method('checkNamedRoute')
473 ->willReturnMap($access_manager_map);
475 $this->factory->expects($this->any())
476 ->method('createInstance')
477 ->will($this->returnValueMap($map));
480 protected function setupNullCacheabilityMetadataValidation() {
481 $container = \Drupal::hasContainer() ? \Drupal::getContainer() : new ContainerBuilder();
483 $cache_context_manager = $this->prophesize(CacheContextsManager::class);
485 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) {
486 $cache_context_manager->assertValidTokens($argument)->willReturn(TRUE);
489 $container->set('cache_contexts_manager', $cache_context_manager->reveal());
490 \Drupal::setContainer($container);