3 namespace Drupal\Tests\system\Functional\Menu;
5 use Drupal\Component\Utility\Html;
7 use Drupal\Tests\BrowserTestBase;
10 * Tests local tasks derived from router and added/altered via hooks.
14 class LocalTasksTest extends BrowserTestBase {
21 public static $modules = ['block', 'menu_test', 'entity_test', 'node'];
24 * The local tasks block under testing.
26 * @var \Drupal\block\Entity\Block
33 protected function setUp() {
36 $this->sut = $this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
40 * Asserts local tasks in the page output.
42 * @param array $routes
43 * A list of expected local tasks, prepared as an array of route names and
44 * their associated route parameters, to assert on the page (in the given
47 * (optional) The local tasks level to assert; 0 for primary, 1 for
48 * secondary. Defaults to 0.
50 protected function assertLocalTasks(array $routes, $level = 0) {
51 $elements = $this->xpath('//*[contains(@class, :class)]//a', [
52 ':class' => $level == 0 ? 'tabs primary' : 'tabs secondary',
54 $this->assertTrue(count($elements), 'Local tasks found.');
55 foreach ($routes as $index => $route_info) {
56 list($route_name, $route_parameters) = $route_info;
57 $expected = Url::fromRoute($route_name, $route_parameters)->toString();
58 $method = ($elements[$index]->getAttribute('href') == $expected ? 'pass' : 'fail');
59 $this->{$method}(format_string('Task @number href @value equals @expected.', [
60 '@number' => $index + 1,
61 '@value' => $elements[$index]->getAttribute('href'),
62 '@expected' => $expected,
68 * Ensures that some local task appears.
70 * @param string $title
74 * TRUE if the local task exists on the page.
76 protected function assertLocalTaskAppers($title) {
77 // SimpleXML gives us the unescaped text, not the actual escaped markup,
78 // so use a pattern instead to check the raw content.
79 // This behaviour is a bug in libxml, see
80 // https://bugs.php.net/bug.php?id=49437.
81 return $this->assertPattern('@<a [^>]*>' . preg_quote($title, '@') . '</a>@');
85 * Asserts that the local tasks on the specified level are not being printed.
88 * (optional) The local tasks level to assert; 0 for primary, 1 for
89 * secondary. Defaults to 0.
91 protected function assertNoLocalTasks($level = 0) {
92 $elements = $this->xpath('//*[contains(@class, :class)]//a', [
93 ':class' => $level == 0 ? 'tabs primary' : 'tabs secondary',
95 $this->assertFalse(count($elements), 'Local tasks not found.');
99 * Tests the plugin based local tasks.
101 public function testPluginLocalTask() {
102 // Verify local tasks defined in the hook.
103 $this->drupalGet(Url::fromRoute('menu_test.tasks_default'));
104 $this->assertLocalTasks([
105 ['menu_test.tasks_default', []],
106 ['menu_test.router_test1', ['bar' => 'unsafe']],
107 ['menu_test.router_test1', ['bar' => '1']],
108 ['menu_test.router_test2', ['bar' => '2']],
111 // Verify that script tags are escaped on output.
112 $title = Html::escape("Task 1 <script>alert('Welcome to the jungle!')</script>");
113 $this->assertLocalTaskAppers($title);
114 $title = Html::escape("<script>alert('Welcome to the derived jungle!')</script>");
115 $this->assertLocalTaskAppers($title);
117 // Verify that local tasks appear as defined in the router.
118 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_view'));
119 $this->assertLocalTasks([
120 ['menu_test.local_task_test_tasks_view', []],
121 ['menu_test.local_task_test_tasks_edit', []],
122 ['menu_test.local_task_test_tasks_settings', []],
123 ['menu_test.local_task_test_tasks_settings_dynamic', []],
126 $title = Html::escape("<script>alert('Welcome to the jungle!')</script>");
127 $this->assertLocalTaskAppers($title);
129 // Ensure the view tab is active.
130 $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
131 $this->assertEqual(1, count($result), 'There is just a single active tab.');
132 $this->assertEqual('View(active tab)', $result[0]->getText(), 'The view tab is active.');
134 // Verify that local tasks in the second level appear.
136 ['menu_test.local_task_test_tasks_settings_sub1', []],
137 ['menu_test.local_task_test_tasks_settings_sub2', []],
138 ['menu_test.local_task_test_tasks_settings_sub3', []],
139 ['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive1']],
140 ['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive2']],
142 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings'));
143 $this->assertLocalTasks($sub_tasks, 1);
145 $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]/a');
146 $this->assertEqual(1, count($result), 'There is just a single active tab.');
147 $this->assertEqual('Settings(active tab)', $result[0]->getText(), 'The settings tab is active.');
149 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings_sub1'));
150 $this->assertLocalTasks($sub_tasks, 1);
152 $result = $this->xpath('//ul[contains(@class, "tabs")]//a[contains(@class, "active")]');
153 $this->assertEqual(2, count($result), 'There are tabs active on both levels.');
154 $this->assertEqual('Settings(active tab)', $result[0]->getText(), 'The settings tab is active.');
155 $this->assertEqual('Dynamic title for TestTasksSettingsSub1(active tab)', $result[1]->getText(), 'The sub1 tab is active.');
157 $this->assertCacheTag('kittens:ragdoll');
158 $this->assertCacheTag('kittens:dwarf-cat');
160 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive1']));
161 $this->assertLocalTasks($sub_tasks, 1);
163 $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]');
164 $this->assertEqual(2, count($result), 'There are tabs active on both levels.');
165 $this->assertEqual('Settings(active tab)', $result[0]->getText(), 'The settings tab is active.');
166 $this->assertEqual('Derive 1(active tab)', $result[1]->getText(), 'The derive1 tab is active.');
168 // Ensures that the local tasks contains the proper 'provider key'
169 $definitions = $this->container->get('plugin.manager.menu.local_task')->getDefinitions();
170 $this->assertEqual($definitions['menu_test.local_task_test_tasks_view']['provider'], 'menu_test');
171 $this->assertEqual($definitions['menu_test.local_task_test_tasks_edit']['provider'], 'menu_test');
172 $this->assertEqual($definitions['menu_test.local_task_test_tasks_settings']['provider'], 'menu_test');
173 $this->assertEqual($definitions['menu_test.local_task_test_tasks_settings_sub1']['provider'], 'menu_test');
174 $this->assertEqual($definitions['menu_test.local_task_test_tasks_settings_sub2']['provider'], 'menu_test');
175 $this->assertEqual($definitions['menu_test.local_task_test_tasks_settings_sub3']['provider'], 'menu_test');
177 // Test that we we correctly apply the active class to tabs where one of the
178 // request attributes is upcast to an entity object.
179 $entity = \Drupal::entityManager()->getStorage('entity_test')->create(['bundle' => 'test']);
182 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_upcasting_sub1', ['entity_test' => '1']));
185 ['menu_test.local_task_test_upcasting_sub1', ['entity_test' => '1']],
186 ['menu_test.local_task_test_upcasting_sub2', ['entity_test' => '1']],
189 $this->assertLocalTasks($tasks, 0);
191 $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]');
192 $this->assertEqual(1, count($result), 'There is one active tab.');
193 $this->assertEqual('upcasting sub1(active tab)', $result[0]->getText(), 'The "upcasting sub1" tab is active.');
195 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_upcasting_sub2', ['entity_test' => '1']));
198 ['menu_test.local_task_test_upcasting_sub1', ['entity_test' => '1']],
199 ['menu_test.local_task_test_upcasting_sub2', ['entity_test' => '1']],
201 $this->assertLocalTasks($tasks, 0);
203 $result = $this->xpath('//ul[contains(@class, "tabs")]//li[contains(@class, "active")]');
204 $this->assertEqual(1, count($result), 'There is one active tab.');
205 $this->assertEqual('upcasting sub2(active tab)', $result[0]->getText(), 'The "upcasting sub2" tab is active.');
209 * Tests that local task blocks are configurable to show a specific level.
211 public function testLocalTaskBlock() {
212 // Remove the default block and create a new one.
213 $this->sut->delete();
215 $this->sut = $this->drupalPlaceBlock('local_tasks_block', [
216 'id' => 'tabs_block',
218 'secondary' => FALSE,
221 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings'));
223 // Verify that local tasks in the first level appear.
224 $this->assertLocalTasks([
225 ['menu_test.local_task_test_tasks_view', []],
226 ['menu_test.local_task_test_tasks_edit', []],
227 ['menu_test.local_task_test_tasks_settings', []],
230 // Verify that local tasks in the second level doesn't appear.
231 $this->assertNoLocalTasks(1);
233 $this->sut->delete();
234 $this->sut = $this->drupalPlaceBlock('local_tasks_block', [
235 'id' => 'tabs_block',
240 $this->drupalGet(Url::fromRoute('menu_test.local_task_test_tasks_settings'));
242 // Verify that local tasks in the first level doesn't appear.
243 $this->assertNoLocalTasks(0);
245 // Verify that local tasks in the second level appear.
247 ['menu_test.local_task_test_tasks_settings_sub1', []],
248 ['menu_test.local_task_test_tasks_settings_sub2', []],
249 ['menu_test.local_task_test_tasks_settings_sub3', []],
250 ['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive1']],
251 ['menu_test.local_task_test_tasks_settings_derived', ['placeholder' => 'derive2']],
253 $this->assertLocalTasks($sub_tasks, 1);
257 * Test that local tasks blocks cache is invalidated correctly.
259 public function testLocalTaskBlockCache() {
260 $this->drupalLogin($this->rootUser);
261 $this->drupalCreateContentType(['type' => 'page']);
263 $this->drupalGet('/admin/structure/types/manage/page');
265 // Only the Edit task. The block avoids showing a single tab.
266 $this->assertNoLocalTasks();
268 // Field UI adds the usual Manage fields etc tabs.
269 \Drupal::service('module_installer')->install(['field_ui']);
271 $this->drupalGet('/admin/structure/types/manage/page');
273 $this->assertLocalTasks([
274 ['entity.node_type.edit_form', ['node_type' => 'page']],
275 ['entity.node.field_ui_fields', ['node_type' => 'page']],
276 ['entity.entity_form_display.node.default', ['node_type' => 'page']],
277 ['entity.entity_view_display.node.default', ['node_type' => 'page']],